@hot-updater/console 1.0.0-rc.9 → 1.0.0

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 (341) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
  3. package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
  7. package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DCM0wr6O.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
  12. package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
  13. package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
  14. package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
  15. package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
  16. package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
  17. package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
  18. package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
  19. package/.output/public/assets/empty-CuT2UhFp.js +1 -0
  20. package/.output/public/assets/field-BBDpjIqu.js +1 -0
  21. package/.output/public/assets/index-6o51GkFe.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-CmraDzQh.js +1 -0
  24. package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
  25. package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
  26. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  27. package/.output/public/assets/insights-vNRCrfnS.js +42 -0
  28. package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
  29. package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
  31. package/.output/public/assets/installations-BazSJXE0.js +2 -0
  32. package/.output/public/assets/installations-ClHTqdAO.js +1 -0
  33. package/.output/public/assets/installations-DpKsBuHi.js +1 -0
  34. package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
  35. package/.output/public/assets/list-CgrYiadM.js +1 -0
  36. package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
  37. package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
  38. package/.output/public/assets/remote-config-Br56BODb.js +6 -0
  39. package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
  40. package/.output/public/assets/routes-B88Erkpk.js +2 -0
  41. package/.output/public/assets/routes-CfzsAleC.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-BZZBXMkr.js +1 -0
  44. package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
  45. package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
  46. package/.output/public/assets/styles-DTGEAJFv.css +2 -0
  47. package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
  55. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  56. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  57. package/.output/server/_libs/lucide-react.mjs +221 -9
  58. package/.output/server/_libs/next-themes.mjs +1 -1
  59. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  60. package/.output/server/_libs/recharts+[...].mjs +483 -901
  61. package/.output/server/_libs/sonner.mjs +1 -1
  62. package/.output/server/_libs/tanstack__history.mjs +68 -41
  63. package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
  64. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  65. package/.output/server/_libs/verkit.mjs +102 -1
  66. package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
  69. package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
  70. package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
  74. package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
  75. package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
  76. package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
  77. package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
  78. package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
  82. package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
  83. package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
  84. package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
  85. package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
  86. package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
  87. package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
  89. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
  90. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
  91. package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
  92. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
  93. package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
  94. package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
  96. package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
  97. package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
  98. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
  99. package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
  100. package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
  103. package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
  104. package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
  105. package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
  118. package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
  119. package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
  120. package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
  121. package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
  122. package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
  125. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  126. package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
  127. package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
  128. package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
  129. package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
  130. package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
  131. package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
  132. package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
  135. package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
  136. package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
  138. package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
  139. package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
  140. package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.mjs} +107 -47
  142. package/.output/server/_ssr/ssr.mjs +1 -1
  143. package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
  144. package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
  145. package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
  150. package/.output/server/index.mjs +309 -211
  151. package/LICENSE +13 -14
  152. package/README.md +80 -71
  153. package/lib/index.d.mts +23 -11
  154. package/lib/vite-internal.mjs +13 -3
  155. package/lib/vite.d.mts +2 -0
  156. package/package.json +14 -14
  157. package/src/components/AppSidebar.tsx +69 -74
  158. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  159. package/src/components/NotFoundPage.tsx +7 -7
  160. package/src/components/features/FeatureSlots.tsx +103 -0
  161. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  162. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  163. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  164. package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
  165. package/src/components/features/insights/AppUsage.tsx +41 -14
  166. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  167. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  168. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  169. package/src/components/features/insights/EventDetails.tsx +390 -26
  170. package/src/components/features/insights/EventHistoryCard.tsx +111 -25
  171. package/src/components/features/insights/EventHistoryList.tsx +1 -1
  172. package/src/components/features/insights/FailureReports.tsx +407 -0
  173. package/src/components/features/insights/InsightsControls.tsx +240 -142
  174. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  175. package/src/components/features/insights/InsightsOverview.tsx +601 -403
  176. package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
  177. package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
  178. package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
  179. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  180. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  181. package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
  182. package/src/components/features/insights/RollbackButton.tsx +118 -0
  183. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  184. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  185. package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
  186. package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
  187. package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
  188. package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
  189. package/src/components/features/remote-config/ParametersCard.tsx +254 -0
  190. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  191. package/src/components/features/remote-config/PublishDialog.tsx +325 -0
  192. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  193. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  194. package/src/components/features/remote-config/ValueInput.tsx +151 -0
  195. package/src/components/features/remote-config/VersionsCard.tsx +384 -0
  196. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  197. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  198. package/src/components/ui/sidebar.tsx +22 -29
  199. package/src/index.ts +26 -19
  200. package/src/lib/api-keys-api.ts +0 -28
  201. package/src/lib/api-keys-rpc.ts +27 -38
  202. package/src/lib/api-rpc.ts +125 -288
  203. package/src/lib/api.ts +15 -134
  204. package/src/lib/bundle-activity.ts +24 -4
  205. package/src/lib/console-access.ts +20 -0
  206. package/src/lib/console-features-api.ts +68 -0
  207. package/src/lib/console-features-rpc.ts +13 -0
  208. package/src/lib/console-features.ts +98 -0
  209. package/src/lib/insights-api.ts +56 -2
  210. package/src/lib/insights-errors.ts +64 -0
  211. package/src/lib/insights-failures-rpc.ts +36 -0
  212. package/src/lib/insights-failures.ts +130 -0
  213. package/src/lib/insights-recovery-rpc.ts +13 -17
  214. package/src/lib/insights-recovery.ts +12 -36
  215. package/src/lib/insights-retention.ts +21 -0
  216. package/src/lib/insights-rpc.ts +30 -12
  217. package/src/lib/insights-search.ts +50 -4
  218. package/src/lib/insights-usage-rpc.ts +15 -9
  219. package/src/lib/insights-usage.ts +41 -3
  220. package/src/lib/insights-view.ts +34 -5
  221. package/src/lib/release-adoption-api.ts +214 -0
  222. package/src/lib/release-adoption-rpc.ts +49 -0
  223. package/src/lib/release-adoption.ts +164 -0
  224. package/src/lib/remote-config-api.ts +119 -0
  225. package/src/lib/remote-config-draft.ts +666 -0
  226. package/src/lib/remote-config-rpc.ts +196 -0
  227. package/src/lib/scroll-restoration.ts +21 -0
  228. package/src/lib/server/auth.server.ts +13 -5
  229. package/src/lib/server/bundleActivity.ts +27 -28
  230. package/src/lib/server/config.server.ts +21 -76
  231. package/src/lib/server/console-feature-apis.ts +70 -0
  232. package/src/lib/server/console-runtime.server.ts +82 -38
  233. package/src/lib/server/downloadBundle.ts +14 -15
  234. package/src/lib/server/failureReports.ts +50 -0
  235. package/src/lib/server/getBundleChildren.ts +31 -122
  236. package/src/lib/server/insightsUsage.ts +52 -113
  237. package/src/lib/server/listReleases.ts +109 -44
  238. package/src/lib/server/releaseAdoption.ts +151 -0
  239. package/src/lib/server/releaseReachability.ts +8 -8
  240. package/src/lib/server/remoteConfig.ts +75 -0
  241. package/src/lib/server/runtime.server.ts +120 -31
  242. package/src/lib/server/updateFailures.ts +66 -0
  243. package/src/lib/use-dialog-target.ts +27 -0
  244. package/src/routeTree.gen.ts +44 -44
  245. package/src/router.tsx +3 -2
  246. package/src/routes/-installations-search.ts +32 -8
  247. package/src/routes/-releases-search.ts +76 -29
  248. package/src/routes/__root.tsx +7 -1
  249. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  250. package/src/routes/api-keys.tsx +5 -2
  251. package/src/routes/index.tsx +210 -152
  252. package/src/routes/insights.tsx +91 -23
  253. package/src/routes/insights_.distribution.tsx +50 -9
  254. package/src/routes/installations.tsx +97 -40
  255. package/src/routes/remote-config.tsx +42 -0
  256. package/src/styles.css +9 -0
  257. package/src/vite-internal.ts +15 -3
  258. package/src/vite.ts +2 -0
  259. package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
  260. package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
  261. package/.output/public/assets/api-CdUyUqzR.js +0 -42
  262. package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
  263. package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
  264. package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
  265. package/.output/public/assets/card-CYHs5FZv.js +0 -1
  266. package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
  267. package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
  268. package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
  269. package/.output/public/assets/dist-BR4GzeEu.js +0 -1
  270. package/.output/public/assets/field-B3CYDpjg.js +0 -1
  271. package/.output/public/assets/index-Cb_D1D6N.js +0 -10
  272. package/.output/public/assets/info-C3y9HNoU.js +0 -1
  273. package/.output/public/assets/input-group-nLZylhWF.js +0 -1
  274. package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
  275. package/.output/public/assets/insights-DS3iO7Up.js +0 -1
  276. package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
  277. package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
  278. package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
  279. package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
  280. package/.output/public/assets/installations-CkFW4fV9.js +0 -2
  281. package/.output/public/assets/installations-DxTaIcHD.js +0 -1
  282. package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
  283. package/.output/public/assets/list-Dqt53fWZ.js +0 -1
  284. package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
  285. package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
  286. package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
  287. package/.output/public/assets/routes-Cgtvspjg.js +0 -2
  288. package/.output/public/assets/routes-O2r2UUN8.js +0 -1
  289. package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
  290. package/.output/public/assets/signing-BgspC-xP.js +0 -1
  291. package/.output/public/assets/styles-DnrWuBwd.css +0 -2
  292. package/.output/public/assets/table-BuWQgxJJ.js +0 -1
  293. package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
  294. package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
  295. package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
  296. package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
  297. package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
  298. package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
  299. package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
  300. package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
  301. package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
  302. package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
  303. package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
  304. package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
  305. package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
  306. package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
  307. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  308. package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
  309. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  310. package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
  311. package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
  312. package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
  313. package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
  314. package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
  315. package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
  316. package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
  317. package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
  318. package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
  319. package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
  320. package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
  321. package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
  322. package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
  323. package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
  324. package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
  325. package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
  326. package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
  327. package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
  328. package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
  329. package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
  330. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  331. package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
  332. package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
  333. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
  334. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  335. package/src/lib/bundle-signing-api.ts +0 -14
  336. package/src/lib/bundle-signing-rpc.ts +0 -16
  337. package/src/lib/server/bundle-signing.server.ts +0 -23
  338. package/src/lib/server/deleteBundle.ts +0 -145
  339. package/src/lib/server/insightsHistory.ts +0 -52
  340. package/src/lib/server/insightsRecovery.ts +0 -187
  341. package/src/routes/signing.tsx +0 -37
@@ -4,7 +4,13 @@ import type { ReactNode } from "react";
4
4
 
5
5
  import { HashValueDisplay } from "@/components/HashValueDisplay";
6
6
  import { Button, buttonVariants } from "@/components/ui/button";
7
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@/components/ui/card";
8
14
  import { Skeleton } from "@/components/ui/skeleton";
9
15
  import {
10
16
  Table,
@@ -14,7 +20,14 @@ import {
14
20
  TableHeader,
15
21
  TableRow,
16
22
  } from "@/components/ui/table";
17
- import type { InsightsEventRow, InsightsViewPage } from "@/lib/insights-view";
23
+ import {
24
+ EVENT_RANGES,
25
+ type EventRange,
26
+ eventRanges,
27
+ type InsightsEventRow,
28
+ type InsightsViewPage,
29
+ widerEventRange,
30
+ } from "@/lib/insights-view";
18
31
 
19
32
  import {
20
33
  EventBundleTransition,
@@ -25,12 +38,21 @@ import {
25
38
  import { EventHistoryList } from "./EventHistoryList";
26
39
  import { InsightsErrorAlert } from "./InsightsErrorAlert";
27
40
  import { InsightsPagination } from "./InsightsPagination";
41
+ import { PeriodSelector } from "./InsightsPeriodSelector";
28
42
 
29
43
  type EventsLocationState = {
30
44
  readonly eventsBefore: number;
31
45
  readonly eventsCursor?: string;
46
+ readonly eventsRange?: EventRange;
32
47
  };
33
48
 
49
+ /** The ranges within the days raw events are kept, the shortest always. */
50
+ const rangesWithin = (days: number) =>
51
+ EVENT_RANGES.filter(
52
+ (value, position) =>
53
+ position === 0 || eventRanges[value].durationMs <= days * 86_400_000,
54
+ );
55
+
34
56
  function EventIdentity({
35
57
  event,
36
58
  eventsLocation,
@@ -62,12 +84,17 @@ function EventIdentity({
62
84
  query: event.installId || undefined,
63
85
  installId: event.installId,
64
86
  }}
65
- aria-label={`View history for ${event.userId ?? event.username ?? "anonymous installation"} (${event.installId})`}
87
+ state={(previous) => ({
88
+ insightsPagination: {
89
+ eventsBack: previous.insightsPagination?.eventsBack,
90
+ },
91
+ })}
92
+ aria-label={`View history for ${event.userId ?? "anonymous installation"} (${event.installId})`}
66
93
  >
67
94
  <span
68
95
  className={touch ? "min-w-0 text-left wrap-anywhere" : "truncate"}
69
96
  >
70
- {event.userId ?? event.username ?? "Anonymous installation"}
97
+ {event.userId ?? "Anonymous installation"}
71
98
  </span>
72
99
  <ArrowUpRight aria-hidden="true" data-icon="inline-end" />
73
100
  </Link>
@@ -82,6 +109,7 @@ function EventIdentity({
82
109
 
83
110
  export function EventHistoryCard({
84
111
  children,
112
+ description,
85
113
  error,
86
114
  eventsLocation,
87
115
  history,
@@ -89,11 +117,16 @@ export function EventHistoryCard({
89
117
  isLoading,
90
118
  onNext,
91
119
  onPrevious,
120
+ onRangeChange,
92
121
  onRefresh,
93
122
  pageNumber,
123
+ range,
124
+ rawDays = 90,
94
125
  title = "All events",
95
126
  }: {
96
127
  readonly children?: ReactNode;
128
+ /** One line under the title that says what the list holds. */
129
+ readonly description?: ReactNode;
97
130
  readonly error: Error | null;
98
131
  readonly eventsLocation: EventsLocationState;
99
132
  readonly history: InsightsViewPage<InsightsEventRow> | undefined;
@@ -101,30 +134,68 @@ export function EventHistoryCard({
101
134
  readonly isLoading: boolean;
102
135
  readonly onNext: () => void;
103
136
  readonly onPrevious: () => void;
137
+ /** Lets the list change its range; a list with a fixed window leaves it out. */
138
+ readonly onRangeChange?: (range: EventRange) => void;
104
139
  readonly onRefresh: () => void;
105
140
  readonly pageNumber: number;
141
+ /** The time range the list reads, named by its empty and end states. */
142
+ readonly range?: EventRange;
143
+ /** How long the server keeps raw events: the longest range offered. */
144
+ readonly rawDays?: number;
106
145
  readonly title?: string;
107
146
  }) {
108
147
  const dateTimeFormat = useInsightsTimeFormat();
109
148
  const hasPrevious = pageNumber > 1;
149
+ const ranges = rangesWithin(rawDays);
150
+ const next = range === undefined ? undefined : widerEventRange(range);
151
+ const wider = next !== undefined && ranges.includes(next) ? next : undefined;
152
+ const widen =
153
+ wider !== undefined && onRangeChange ? (
154
+ <Button
155
+ className="h-11 px-3 lg:h-8"
156
+ onClick={() => onRangeChange(wider)}
157
+ type="button"
158
+ variant="outline"
159
+ >
160
+ {`Show the last ${eventRanges[wider].period}`}
161
+ </Button>
162
+ ) : null;
110
163
 
111
164
  return (
112
165
  <Card className="@container min-w-0 shadow-sm" aria-busy={isFetching}>
113
166
  <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>
167
+ <div className="flex flex-col gap-1.5">
168
+ <div className="flex flex-wrap items-center justify-between gap-3">
169
+ <CardTitle>
170
+ <h2>{title}</h2>
171
+ </CardTitle>
172
+ <div className="flex flex-wrap items-center gap-2">
173
+ {range !== undefined && onRangeChange ? (
174
+ <PeriodSelector
175
+ label="Time range"
176
+ onPeriodChange={onRangeChange}
177
+ period={range}
178
+ periods={ranges.map((value) => ({
179
+ value,
180
+ name: `Last ${eventRanges[value].period}`,
181
+ }))}
182
+ />
183
+ ) : null}
184
+ <Button
185
+ className="h-11 lg:h-8"
186
+ disabled={isFetching}
187
+ onClick={onRefresh}
188
+ size="lg"
189
+ variant="outline"
190
+ >
191
+ <RefreshCw aria-hidden="true" data-icon="inline-start" />
192
+ Refresh
193
+ </Button>
194
+ </div>
195
+ </div>
196
+ {description ? (
197
+ <CardDescription>{description}</CardDescription>
198
+ ) : null}
128
199
  </div>
129
200
  {children}
130
201
  </CardHeader>
@@ -189,7 +260,7 @@ export function EventHistoryCard({
189
260
  />
190
261
  </TableCell>
191
262
  <TableCell>
192
- <EventTypeDetails type={event.type} />
263
+ <EventTypeDetails event={event} />
193
264
  </TableCell>
194
265
  <TableCell>
195
266
  <EventIdentity
@@ -218,14 +289,29 @@ export function EventHistoryCard({
218
289
  </div>
219
290
  </>
220
291
  ) : (
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"}
292
+ <div className="flex flex-wrap items-center justify-between gap-3 px-4 pb-4 sm:px-6 sm:pb-6">
293
+ <p className="text-sm text-muted-foreground">
294
+ {hasPrevious
295
+ ? "No older events in this range."
296
+ : range !== undefined
297
+ ? `No events in the last ${eventRanges[range].period}.`
298
+ : title === "All events"
299
+ ? "No events recorded yet"
300
+ : "No matching reports in this period"}
301
+ </p>
302
+ {hasPrevious ? null : widen}
227
303
  </div>
228
304
  )}
305
+ {range !== undefined &&
306
+ history.nextCursor === null &&
307
+ (history.data.length > 0 || hasPrevious) ? (
308
+ <div className="flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3 sm:px-6">
309
+ <p className="text-sm text-muted-foreground">
310
+ Start of range reached
311
+ </p>
312
+ {widen}
313
+ </div>
314
+ ) : null}
229
315
  {history.data.length > 0 || hasPrevious || history.nextCursor ? (
230
316
  <InsightsPagination
231
317
  hasPrevious={hasPrevious}
@@ -29,7 +29,7 @@ export function EventHistoryList<T extends InsightsEventRow>({
29
29
  className="flex min-w-0 flex-col gap-3 px-4 py-4 sm:px-6"
30
30
  >
31
31
  <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
32
- <EventTypeDetails type={event.type} />
32
+ <EventTypeDetails event={event} />
33
33
  <div className="text-xs tabular-nums">
34
34
  <EventTimestamp
35
35
  value={event.receivedAtMs}
@@ -0,0 +1,407 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ArrowUpRight, Copy, RefreshCw } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Empty,
9
+ EmptyDescription,
10
+ EmptyHeader,
11
+ EmptyTitle,
12
+ } from "@/components/ui/empty";
13
+ import {
14
+ Sheet,
15
+ SheetContent,
16
+ SheetDescription,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ SheetTrigger,
20
+ } from "@/components/ui/sheet";
21
+ import { Skeleton } from "@/components/ui/skeleton";
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableHead,
27
+ TableHeader,
28
+ TableRow,
29
+ } from "@/components/ui/table";
30
+ import { useFailureReportsQuery } from "@/lib/insights-api";
31
+ import {
32
+ groupFailureReports,
33
+ type FailureGroup,
34
+ type FailureReportsInput,
35
+ type FailureReportsPage,
36
+ } from "@/lib/insights-errors";
37
+ import {
38
+ failureDetailParts,
39
+ failureReasonLabel,
40
+ failureStageLabel,
41
+ } from "@/lib/insights-failures";
42
+ import type { InsightsEventRow } from "@/lib/insights-view";
43
+ import { cn } from "@/lib/utils";
44
+
45
+ import {
46
+ EventBundleTransition,
47
+ EventTimestamp,
48
+ HttpResponseBody,
49
+ useInsightsTimeFormat,
50
+ } from "./EventDetails";
51
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
52
+
53
+ const messageOf = (event: InsightsEventRow) =>
54
+ event.failure?.errorMessage || "No error message reported";
55
+
56
+ function FailureDetail({ group }: { readonly group: FailureGroup }) {
57
+ const [selectedId, setSelectedId] = useState(group.reports[0]!.id);
58
+ const event =
59
+ group.reports.find(({ id }) => id === selectedId) ?? group.reports[0]!;
60
+ const failure = event.failure!;
61
+ const formatter = useInsightsTimeFormat();
62
+ const context = [
63
+ [
64
+ "App",
65
+ `${event.platform === "ios" ? "iOS" : "Android"} ${event.appVersion}`,
66
+ ],
67
+ ["Channel", event.channel],
68
+ ["SDK", event.sdkVersion ?? "Not reported"],
69
+ ["Stage", failureStageLabel(failure.stage)],
70
+ ["Resource", failure.resource],
71
+ ["HTTP status", failure.httpStatus?.toString()],
72
+ ["Origin code", failure.originCode],
73
+ ["Transport", failure.transport],
74
+ ].filter(([, value]) => value !== undefined);
75
+ const copy = async () => {
76
+ try {
77
+ await navigator.clipboard.writeText(JSON.stringify(event, null, 2));
78
+ toast.success("Error report copied");
79
+ } catch {
80
+ toast.error("Could not copy the report");
81
+ }
82
+ };
83
+ return (
84
+ <SheetContent className="data-[side=right]:w-full data-[side=right]:sm:max-w-2xl">
85
+ <SheetHeader className="border-b pr-14">
86
+ <SheetTitle>Error details</SheetTitle>
87
+ <SheetDescription>
88
+ {group.reports.length.toLocaleString()} loaded reports ·{" "}
89
+ {group.installations.toLocaleString()} installations
90
+ </SheetDescription>
91
+ </SheetHeader>
92
+ <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-6">
93
+ <section
94
+ aria-label="Original error"
95
+ className="flex min-w-0 flex-col gap-3"
96
+ >
97
+ <p className="text-xs text-muted-foreground">
98
+ {failureStageLabel(failure.stage)} ·{" "}
99
+ {failureReasonLabel(failure.reason)}
100
+ </p>
101
+ <h3
102
+ className={cn(
103
+ "text-sm leading-relaxed whitespace-pre-wrap wrap-anywhere",
104
+ failure.errorMessage ? "font-mono" : "text-muted-foreground",
105
+ )}
106
+ >
107
+ {messageOf(event)}
108
+ </h3>
109
+ {!failure.errorMessage ? (
110
+ <p className="text-sm text-muted-foreground">
111
+ This client did not send the original error. Upgrade the app’s
112
+ Insights plugin and server to collect details for future failures.
113
+ </p>
114
+ ) : null}
115
+ <Button
116
+ className="w-fit"
117
+ variant="outline"
118
+ onClick={() => void copy()}
119
+ >
120
+ <Copy aria-hidden="true" data-icon="inline-start" /> Copy report
121
+ </Button>
122
+ </section>
123
+ {event.httpResponse ? (
124
+ <section
125
+ aria-label="Server response"
126
+ className="flex min-w-0 flex-col gap-3"
127
+ >
128
+ <h3 className="text-sm font-medium">
129
+ Server response · HTTP {event.httpResponse.status}
130
+ </h3>
131
+ <p className="text-xs text-muted-foreground">
132
+ Most recent response available when this report was created.
133
+ </p>
134
+ <p className="font-mono text-xs wrap-anywhere">
135
+ GET {event.httpResponse.path}
136
+ </p>
137
+ <div className="text-xs text-muted-foreground">
138
+ Response received{" "}
139
+ <EventTimestamp
140
+ value={event.httpResponse.receivedAtMs}
141
+ formatter={formatter}
142
+ />
143
+ </div>
144
+ <HttpResponseBody response={event.httpResponse} />
145
+ </section>
146
+ ) : null}
147
+ <section
148
+ aria-label="Stack trace"
149
+ className="flex min-w-0 flex-col gap-2"
150
+ >
151
+ <h3 className="text-sm font-medium">Stack trace</h3>
152
+ {failure.errorStack ? (
153
+ <pre className="rounded-md border bg-muted/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap wrap-anywhere">
154
+ {failure.errorStack}
155
+ </pre>
156
+ ) : (
157
+ <p className="text-sm text-muted-foreground">
158
+ No stack trace was sent with this report.
159
+ </p>
160
+ )}
161
+ </section>
162
+ <section aria-label="Report context" className="flex flex-col gap-3">
163
+ <h3 className="text-sm font-medium">Report context</h3>
164
+ <div className="text-xs text-muted-foreground">
165
+ <EventTimestamp value={event.receivedAtMs} formatter={formatter} />
166
+ </div>
167
+ <dl className="grid grid-cols-2 gap-x-6 gap-y-4">
168
+ {context.map(([label, value]) => (
169
+ <div key={label} className="min-w-0">
170
+ <dt className="text-xs text-muted-foreground">{label}</dt>
171
+ <dd className="mt-1 text-sm wrap-anywhere">{value}</dd>
172
+ </div>
173
+ ))}
174
+ </dl>
175
+ <EventBundleTransition event={event} />
176
+ <Button
177
+ variant="outline"
178
+ className="w-fit"
179
+ render={
180
+ <Link
181
+ to="/installations"
182
+ search={{ query: event.installId, installId: event.installId }}
183
+ />
184
+ }
185
+ >
186
+ Installation history{" "}
187
+ <ArrowUpRight aria-hidden="true" data-icon="inline-end" />
188
+ </Button>
189
+ <p className="font-mono text-xs text-muted-foreground wrap-anywhere">
190
+ {event.installId}
191
+ </p>
192
+ </section>
193
+ <section aria-label="Occurrences" className="flex flex-col gap-2">
194
+ <h3 className="text-sm font-medium">Occurrences in loaded reports</h3>
195
+ <p className="text-xs text-muted-foreground">
196
+ Select a report to inspect its stack and context. Times use your
197
+ browser’s zone.
198
+ </p>
199
+ <div className="flex flex-col gap-1">
200
+ {group.reports.map((report) => (
201
+ <Button
202
+ key={report.id}
203
+ variant={report.id === event.id ? "secondary" : "ghost"}
204
+ aria-pressed={report.id === event.id}
205
+ className="h-auto min-h-11 justify-between gap-4 whitespace-normal"
206
+ onClick={() => setSelectedId(report.id)}
207
+ >
208
+ <span className="text-left tabular-nums">
209
+ {formatter.format(report.receivedAtMs)}
210
+ </span>
211
+ <span className="shrink-0">{report.appVersion}</span>
212
+ </Button>
213
+ ))}
214
+ </div>
215
+ </section>
216
+ </div>
217
+ </SheetContent>
218
+ );
219
+ }
220
+
221
+ export function FailureReportsList({
222
+ pages,
223
+ error,
224
+ isPending,
225
+ isFetching,
226
+ hasNextPage,
227
+ onLoadMore,
228
+ onRefresh,
229
+ }: {
230
+ readonly pages: readonly FailureReportsPage[];
231
+ readonly error: Error | null;
232
+ readonly isPending: boolean;
233
+ readonly isFetching: boolean;
234
+ readonly hasNextPage: boolean;
235
+ readonly onLoadMore: () => void;
236
+ readonly onRefresh: () => void;
237
+ }) {
238
+ const groups = groupFailureReports(pages.flatMap((page) => page.data));
239
+ const reportCount = groups.reduce(
240
+ (sum, group) => sum + group.reports.length,
241
+ 0,
242
+ );
243
+ const scanned = pages.reduce((sum, page) => sum + page.scanned, 0);
244
+ const formatter = useInsightsTimeFormat();
245
+ return (
246
+ <section
247
+ aria-label="Errors to investigate"
248
+ className="flex min-w-0 flex-col gap-3"
249
+ aria-busy={isFetching}
250
+ >
251
+ <div className="flex items-center justify-between gap-4">
252
+ <div className="flex flex-col gap-1">
253
+ <h3 className="text-sm font-medium">Errors to investigate</h3>
254
+ <p className="text-xs text-muted-foreground">
255
+ Original messages, most reported first. Select an error for its
256
+ stack and affected app.
257
+ </p>
258
+ </div>
259
+ <Button
260
+ variant="ghost"
261
+ size="icon-lg"
262
+ aria-label="Refresh error reports"
263
+ disabled={isFetching}
264
+ onClick={onRefresh}
265
+ >
266
+ <RefreshCw aria-hidden="true" />
267
+ </Button>
268
+ </div>
269
+ {isPending ? (
270
+ <Skeleton className="h-40" aria-label="Loading error reports" />
271
+ ) : (
272
+ <>
273
+ {error ? (
274
+ <InsightsErrorAlert
275
+ error={error}
276
+ fallbackTitle="Error reports unavailable"
277
+ />
278
+ ) : null}
279
+ {groups.length ? (
280
+ <Table aria-label="Error messages">
281
+ <TableHeader>
282
+ <TableRow>
283
+ <TableHead>Error</TableHead>
284
+ <TableHead className="text-right">Reports</TableHead>
285
+ <TableHead className="hidden text-right sm:table-cell">
286
+ Installations
287
+ </TableHead>
288
+ <TableHead className="hidden lg:table-cell">
289
+ Last seen
290
+ </TableHead>
291
+ </TableRow>
292
+ </TableHeader>
293
+ <TableBody>
294
+ {groups.map((group) => {
295
+ const latest = group.reports[0]!;
296
+ const failure = latest.failure!;
297
+ return (
298
+ <TableRow key={group.key}>
299
+ <TableCell className="max-w-md whitespace-normal">
300
+ <Sheet>
301
+ <SheetTrigger
302
+ render={
303
+ <Button
304
+ variant="link"
305
+ className="h-auto min-h-11 w-full justify-start whitespace-normal px-0 text-left"
306
+ />
307
+ }
308
+ >
309
+ <span className="flex min-w-0 flex-col gap-1">
310
+ <span
311
+ className={cn(
312
+ "line-clamp-2 text-xs leading-relaxed wrap-anywhere",
313
+ latest.failure?.errorMessage
314
+ ? "font-mono"
315
+ : "text-muted-foreground",
316
+ )}
317
+ >
318
+ {messageOf(latest)}
319
+ </span>
320
+ <span className="text-xs font-normal text-muted-foreground">
321
+ {[
322
+ failureStageLabel(failure.stage),
323
+ ...failureDetailParts(failure),
324
+ ].join(" · ")}
325
+ </span>
326
+ </span>
327
+ </SheetTrigger>
328
+ <FailureDetail group={group} />
329
+ </Sheet>
330
+ </TableCell>
331
+ <TableCell className="text-right font-medium tabular-nums">
332
+ {group.reports.length.toLocaleString()}
333
+ </TableCell>
334
+ <TableCell className="hidden text-right tabular-nums sm:table-cell">
335
+ {group.installations.toLocaleString()}
336
+ </TableCell>
337
+ <TableCell className="hidden text-xs text-muted-foreground tabular-nums lg:table-cell">
338
+ {formatter.format(latest.receivedAtMs)}
339
+ </TableCell>
340
+ </TableRow>
341
+ );
342
+ })}
343
+ </TableBody>
344
+ </Table>
345
+ ) : !error ? (
346
+ <Empty>
347
+ <EmptyHeader>
348
+ <EmptyTitle>
349
+ {hasNextPage
350
+ ? "No failures in the events loaded so far"
351
+ : "No error reports in this range"}
352
+ </EmptyTitle>
353
+ <EmptyDescription>
354
+ {hasNextPage
355
+ ? "Load older events to continue investigating this period."
356
+ : "Reports appear here when a client sends an update failure. Raw history may be shorter than aggregate history."}
357
+ </EmptyDescription>
358
+ </EmptyHeader>
359
+ </Empty>
360
+ ) : null}
361
+ <div className="flex flex-wrap items-center justify-between gap-3">
362
+ <p className="text-xs text-muted-foreground" role="status">
363
+ {reportCount.toLocaleString()} failure reports from{" "}
364
+ {scanned.toLocaleString()} events reviewed.
365
+ {hasNextPage
366
+ ? " More events remain; these counts are not period totals."
367
+ : error
368
+ ? " Scan interrupted."
369
+ : " End of available range."}
370
+ {pages[0]
371
+ ? ` Raw history since ${formatter.format(pages[0].sinceMs)}.`
372
+ : ""}
373
+ </p>
374
+ {hasNextPage ? (
375
+ <Button
376
+ variant="outline"
377
+ disabled={isFetching}
378
+ onClick={onLoadMore}
379
+ >
380
+ {isFetching ? "Loading…" : "Load older events"}
381
+ </Button>
382
+ ) : null}
383
+ </div>
384
+ </>
385
+ )}
386
+ </section>
387
+ );
388
+ }
389
+
390
+ export function FailureReports({
391
+ input,
392
+ }: {
393
+ readonly input: FailureReportsInput;
394
+ }) {
395
+ const query = useFailureReportsQuery(input, true);
396
+ return (
397
+ <FailureReportsList
398
+ pages={query.data?.pages ?? []}
399
+ error={query.error}
400
+ isPending={query.isPending}
401
+ isFetching={query.isFetching}
402
+ hasNextPage={query.hasNextPage}
403
+ onLoadMore={() => void query.fetchNextPage()}
404
+ onRefresh={() => void query.refetch()}
405
+ />
406
+ );
407
+ }