@hot-updater/console 0.36.7 → 1.0.0-rc.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 (266) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +1 -0
  3. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
  4. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
  5. package/.output/public/assets/alert-C-1KvDW8.js +1 -0
  6. package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
  7. package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
  8. package/.output/public/assets/api-B_Rh-uzC.js +1 -0
  9. package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
  10. package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
  11. package/.output/public/assets/badge-Bf0fZ-Gx.js +1 -0
  12. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  13. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  14. package/.output/public/assets/dialog-BS60BRr2.js +1 -0
  15. package/.output/public/assets/dist-DspNKfMj.js +1 -0
  16. package/.output/public/assets/download-O-yNBc9H.js +1 -0
  17. package/.output/public/assets/field-Buq148-A.js +1 -0
  18. package/.output/public/assets/index-kFhd2S78.js +10 -0
  19. package/.output/public/assets/installations-BrQ-od41.js +2 -0
  20. package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
  21. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
  22. package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
  23. package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
  24. package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
  25. package/.output/public/assets/routes-CrdokJ80.js +1 -0
  26. package/.output/public/assets/routes-DcQnVajO.js +2 -0
  27. package/.output/public/assets/search-DLhbWw3W.js +1 -0
  28. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  29. package/.output/public/assets/signing-HRexfjsH.js +1 -0
  30. package/.output/public/assets/styles-eB4EtvFS.css +2 -0
  31. package/.output/public/assets/table-D8CLieHX.js +1 -0
  32. package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
  33. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  34. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
  35. package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs +146 -0
  36. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  37. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  38. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  39. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  40. package/.output/server/_libs/d3-array.mjs +231 -0
  41. package/.output/server/_libs/d3-color.mjs +330 -0
  42. package/.output/server/_libs/d3-format.mjs +252 -0
  43. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  44. package/.output/server/_libs/d3-path.mjs +77 -0
  45. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  46. package/.output/server/_libs/d3-shape.mjs +818 -0
  47. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  48. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  49. package/.output/server/_libs/eventemitter3.mjs +236 -0
  50. package/.output/server/_libs/immer.mjs +532 -0
  51. package/.output/server/_libs/lucide-react.mjs +141 -13
  52. package/.output/server/_libs/next-themes.mjs +2 -2
  53. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  54. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  55. package/.output/server/_libs/sonner.mjs +2 -2
  56. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  57. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  58. package/.output/server/_runtime.mjs +10 -1
  59. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
  60. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
  61. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
  62. package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
  63. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
  64. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
  65. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
  66. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
  67. package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
  68. package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
  69. package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
  70. package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
  71. package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
  72. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  73. package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
  74. package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
  75. package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
  76. package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
  77. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
  78. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  79. package/.output/server/_ssr/config.server-DEiLhKmy.mjs +361 -0
  80. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  81. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  82. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-9P0S0lJ3.mjs} +68 -50
  83. package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
  84. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  85. package/.output/server/_ssr/dialog-CQuRMNZ4.mjs +170 -0
  86. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  87. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  88. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  89. package/.output/server/_ssr/field-DRIXKjZG.mjs +74 -0
  90. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  91. package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
  92. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
  93. package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
  94. package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
  95. package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
  96. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
  97. package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
  98. package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
  99. package/.output/server/_ssr/ssr.mjs +4 -5740
  100. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  101. package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
  102. package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
  103. package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
  104. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
  105. package/.output/server/index.mjs +256 -53
  106. package/README.md +29 -8
  107. package/lib/index.d.mts +36 -0
  108. package/lib/index.mjs +4 -0
  109. package/lib/vite-internal.mjs +127 -0
  110. package/lib/vite.d.mts +10 -0
  111. package/lib/vite.mjs +5 -0
  112. package/package.json +51 -29
  113. package/public/apple-touch-icon.png +0 -0
  114. package/public/favicon-16x16.png +0 -0
  115. package/public/favicon-32x32.png +0 -0
  116. package/public/favicon.ico +0 -0
  117. package/public/logo.svg +1 -0
  118. package/public/manifest.json +30 -0
  119. package/public/robots.txt +3 -0
  120. package/src/components/AppSidebar.tsx +159 -0
  121. package/src/components/BundleIdDisplay.tsx +18 -0
  122. package/src/components/ChannelBadge.tsx +31 -0
  123. package/src/components/ConsoleAccessPage.tsx +122 -0
  124. package/src/components/EnabledStatusIcon.tsx +54 -0
  125. package/src/components/HashValueDisplay.tsx +85 -0
  126. package/src/components/HotUpdaterLogo.tsx +30 -0
  127. package/src/components/NotFoundPage.tsx +33 -0
  128. package/src/components/PlatformIcon.tsx +39 -0
  129. package/src/components/RolloutPercentageBadge.tsx +28 -0
  130. package/src/components/ThemeProvider.tsx +116 -0
  131. package/src/components/TimestampDisplay.tsx +41 -0
  132. package/src/components/features/analytics/ActivityChart.tsx +150 -0
  133. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
  134. package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
  135. package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
  136. package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
  137. package/src/components/features/analytics/BundleDistribution.tsx +133 -0
  138. package/src/components/features/analytics/BundleSelector.tsx +114 -0
  139. package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
  140. package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
  141. package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
  142. package/src/components/features/analytics/InstallationPagination.tsx +48 -0
  143. package/src/components/features/analytics/InstallationSearch.tsx +230 -0
  144. package/src/components/features/analytics/UpdateOutcomes.tsx +147 -0
  145. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  146. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  147. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +123 -0
  148. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  149. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  150. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  151. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  152. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  153. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  154. package/src/components/features/signing/BundleSigningPage.tsx +247 -0
  155. package/src/components/ui/alert-dialog.tsx +185 -0
  156. package/src/components/ui/alert.tsx +76 -0
  157. package/src/components/ui/badge.tsx +49 -0
  158. package/src/components/ui/button.tsx +58 -0
  159. package/src/components/ui/card.tsx +89 -0
  160. package/src/components/ui/chart.tsx +371 -0
  161. package/src/components/ui/combobox.tsx +303 -0
  162. package/src/components/ui/dialog.tsx +156 -0
  163. package/src/components/ui/dropdown-menu.tsx +270 -0
  164. package/src/components/ui/field.tsx +235 -0
  165. package/src/components/ui/input-group.tsx +147 -0
  166. package/src/components/ui/input.tsx +19 -0
  167. package/src/components/ui/label.tsx +25 -0
  168. package/src/components/ui/progress.tsx +84 -0
  169. package/src/components/ui/select.tsx +205 -0
  170. package/src/components/ui/separator.tsx +23 -0
  171. package/src/components/ui/sheet.tsx +132 -0
  172. package/src/components/ui/sidebar.tsx +779 -0
  173. package/src/components/ui/skeleton.tsx +13 -0
  174. package/src/components/ui/slider.tsx +56 -0
  175. package/src/components/ui/sonner.tsx +45 -0
  176. package/src/components/ui/switch.tsx +25 -0
  177. package/src/components/ui/table.tsx +114 -0
  178. package/src/components/ui/textarea.tsx +18 -0
  179. package/src/components/ui/toggle-group.tsx +89 -0
  180. package/src/components/ui/toggle.tsx +43 -0
  181. package/src/components/ui/tooltip.tsx +66 -0
  182. package/src/hooks/use-mobile.ts +30 -0
  183. package/src/index.ts +48 -0
  184. package/src/lib/analytics-api.ts +145 -0
  185. package/src/lib/analytics-error.ts +19 -0
  186. package/src/lib/analytics-input.ts +187 -0
  187. package/src/lib/analytics-overview.ts +170 -0
  188. package/src/lib/analytics-rpc.ts +218 -0
  189. package/src/lib/api-keys-api.ts +69 -0
  190. package/src/lib/api-keys-rpc.ts +87 -0
  191. package/src/lib/api-rpc.ts +478 -0
  192. package/src/lib/api.ts +444 -0
  193. package/src/lib/auth-rpc.ts +16 -0
  194. package/src/lib/bundle-signing-api.ts +14 -0
  195. package/src/lib/bundle-signing-rpc.ts +16 -0
  196. package/src/lib/constants.ts +1 -0
  197. package/src/lib/git.ts +51 -0
  198. package/src/lib/server/auth.server.ts +43 -0
  199. package/src/lib/server/bundle-signing.server.ts +100 -0
  200. package/src/lib/server/config.server.ts +109 -0
  201. package/src/lib/server/console-runtime.server.ts +70 -0
  202. package/src/lib/server/deleteBundle.ts +145 -0
  203. package/src/lib/server/downloadBundle.ts +46 -0
  204. package/src/lib/server/getBundleChildren.ts +130 -0
  205. package/src/lib/server/listReleases.ts +61 -0
  206. package/src/lib/server/releaseActivity.ts +27 -0
  207. package/src/lib/server/releaseReachability.ts +95 -0
  208. package/src/lib/server/runtime.server.ts +172 -0
  209. package/src/lib/utils.ts +9 -0
  210. package/src/logo.svg +1 -0
  211. package/src/routeTree.gen.ts +199 -0
  212. package/src/router.tsx +21 -0
  213. package/src/routes/-installations-search.ts +19 -0
  214. package/src/routes/-releases-search.ts +74 -0
  215. package/src/routes/__root.tsx +174 -0
  216. package/src/routes/analytics.tsx +139 -0
  217. package/src/routes/api/auth/$.ts +19 -0
  218. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  219. package/src/routes/api-keys.tsx +37 -0
  220. package/src/routes/index.tsx +982 -0
  221. package/src/routes/installations.tsx +194 -0
  222. package/src/routes/signing.tsx +37 -0
  223. package/src/styles.css +152 -0
  224. package/src/virtual-modules.d.ts +21 -0
  225. package/src/vite-env.d.ts +1 -0
  226. package/src/vite-internal.ts +195 -0
  227. package/src/vite.ts +12 -0
  228. package/.output/public/assets/dist-DJX53faP.js +0 -3
  229. package/.output/public/assets/index-Iylis5kY.js +0 -10
  230. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  231. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  232. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  233. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  234. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  235. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  236. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  237. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  238. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  239. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  240. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  241. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  242. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  243. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  244. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  245. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  246. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  247. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  248. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  249. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  250. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  251. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  252. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  253. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  254. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  255. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  256. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  257. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  258. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  259. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  260. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  261. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  262. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  263. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  264. package/.output/server/node_modules/tslib/package.json +0 -47
  265. package/.output/server/node_modules/tslib/tslib.js +0 -484
  266. package/.output/server/package.json +0 -9
@@ -0,0 +1,2286 @@
1
+ import { i as __toESM } from "../_runtime.mjs";
2
+ import { B as DialogClose, I as DialogTitle, L as DialogPortal, R as DialogPopup, S as DialogRoot, V as DialogBackdrop, _ as SelectPortal, a as SliderTrack, at as require_react, b as SelectTrigger$1, c as SelectGroup$1, d as SelectItemText, f as SelectItemIndicator, g as SelectPositioner, h as SelectPopup, i as SliderThumb, it as require_jsx_runtime, l as SelectScrollUpArrow, m as SelectList, n as SwitchRoot, o as SliderControl, p as SelectItem$1, r as SliderIndicator, s as SliderRoot, t as SwitchThumb, u as SelectScrollDownArrow, v as SelectIcon, x as SelectRoot, y as SelectValue$1, z as DialogDescription } from "../_libs/@base-ui/react+[...].mjs";
3
+ import { n as useAnalyticsCapability } from "./AnalyticsCapabilityContext-ui4DX8GO.mjs";
4
+ import { A as ChevronRight, C as FingerprintPattern, D as CircleOff, F as ArrowRight, M as ChevronDown, N as Check, S as Funnel, T as Download, _ as Minus, a as Tag, f as Plus, i as Tags, j as ChevronLeft, k as ChevronUp, m as Package, n as TriangleAlert, r as Trash2, t as X, u as RotateCcw, v as LoaderCircle, w as ExternalLink, y as List } from "../_libs/lucide-react.mjs";
5
+ import { C as cn, S as buttonVariants, T as useIsMobile, _ as Skeleton, g as SidebarTrigger, n as Input, r as Separator$1, t as Button$1, v as Tooltip$1, x as TooltipTrigger, y as TooltipContent } from "./createSsrRpc-9P0S0lJ3.mjs";
6
+ import { p as Link } from "../_libs/@tanstack/react-router+[...].mjs";
7
+ import { a as FieldLabel, i as FieldGroup, n as FieldDescription, o as Label, r as FieldError, t as Field } from "./field-DRIXKjZG.mjs";
8
+ import { t as Textarea } from "./textarea-DYHnskhb.mjs";
9
+ import { S as useUpdateReleaseMutation, _ as usePreflightReleaseMutation, a as InputGroupText, b as useReleaseQuery, c as useBundleEventAnalyticsQuery, d as useConfigQuery, f as useCreateChannelMutation, i as InputGroupInput, l as useBundleQuery, m as useDeleteReleaseMutation, n as InputGroupAddon, o as useBundleChildCountsQuery, p as useDeleteChannelMutation, s as useBundleChildrenQuery, t as InputGroup, u as useChannelsQuery, v as usePromoteReleaseMutation, x as useReleasesQuery, y as useReleaseCatalogDiagnosticsQuery } from "./api-BTSv1tZb.mjs";
10
+ import { a as Card, i as AlertTitle, l as CardHeader, o as CardContent, r as AlertDescription, s as CardDescription, t as Alert, u as CardTitle } from "./alert-Dim3MGY5.mjs";
11
+ import { n as BundleActivityChart, t as AnalyticsErrorAlert } from "./BundleActivityChart-BiB9ZORH.mjs";
12
+ import { C as normalizeCohortValue, S as isValidCohort, f as getNumericCohortRolloutPosition, g as getPatchFileHash, h as getPatchBaseFileHash, m as getPatchBaseBundleId, r as NUMERIC_COHORT_SIZE, t as INVALID_COHORT_ERROR_MESSAGE, w as normalizeRolloutCohortCount } from "./dist-BVSLTpoP.mjs";
13
+ import { _ as DialogTrigger, a as AlertDialogDescription, d as Dialog$1, f as DialogContent, g as DialogTitle$1, h as DialogHeader, i as AlertDialogContent, l as AlertDialogTitle, m as DialogFooter, n as AlertDialogAction, o as AlertDialogFooter, p as DialogDescription$1, r as AlertDialogCancel, s as AlertDialogHeader, t as AlertDialog$1 } from "./dialog-CQuRMNZ4.mjs";
14
+ import { t as Badge } from "./badge-DCxbEeof.mjs";
15
+ import { a as TableHeader, i as TableHead, n as TableBody, o as TableRow, r as TableCell, t as Table } from "./table-v1yCvYo7.mjs";
16
+ import { n as toast } from "../_libs/sonner.mjs";
17
+ import { r as extractTimestampFromUUIDv7, t as createUUIDv7 } from "./uuidv7-BbJ-9D_I.mjs";
18
+ import { t as BundleIdDisplay } from "./BundleIdDisplay-Czk6iR6p.mjs";
19
+ import { n as updateReleaseFilters, t as Route } from "./routes-Z7TwC6nC.mjs";
20
+ import { n as require_dayjs_min, t as require_relativeTime } from "../_libs/dayjs.mjs";
21
+ import { t as normalizeRange } from "../_libs/verkit.mjs";
22
+ //#region node_modules/.nitro/vite/services/ssr/assets/routes-BM4ht9jb.js
23
+ var import_react = /* @__PURE__ */ __toESM(require_react());
24
+ var import_jsx_runtime = require_jsx_runtime();
25
+ var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min());
26
+ var import_relativeTime = /* @__PURE__ */ __toESM(require_relativeTime());
27
+ var channelColors = {
28
+ production: "bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20",
29
+ dev: "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20",
30
+ staging: "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-500/20"
31
+ };
32
+ function ChannelBadge({ channel, className }) {
33
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
34
+ variant: "outline",
35
+ className: cn(channelColors[channel.toLowerCase()] || "bg-gray-500/10 text-gray-700 dark:text-gray-400 border-gray-500/20", className),
36
+ children: channel
37
+ });
38
+ }
39
+ function EnabledStatusIcon({ enabled, className, falseIcon = "x", colorMode = "semantic" }) {
40
+ const iconColorClassName = colorMode === "inherit" ? "text-current" : void 0;
41
+ if (enabled) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: cn("h-4 w-4", iconColorClassName ?? "text-green-600 dark:text-green-400", className) });
42
+ if (falseIcon === "minus") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Minus, { className: cn("h-4 w-4", iconColorClassName ?? "text-muted-foreground", className) });
43
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { className: cn("h-4 w-4", iconColorClassName ?? "text-red-600 dark:text-red-400", className) });
44
+ }
45
+ import_dayjs_min.default.extend(import_relativeTime.default);
46
+ function TimestampDisplay({ uuid, format = "relative" }) {
47
+ const date = (0, import_dayjs_min.default)(extractTimestampFromUUIDv7(uuid));
48
+ if (format === "relative") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
49
+ className: "text-sm text-muted-foreground",
50
+ children: date.fromNow()
51
+ });
52
+ if (format === "absolute") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
53
+ className: "text-sm text-muted-foreground",
54
+ children: date.format("YYYY-MM-DD HH:mm:ss")
55
+ });
56
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
57
+ className: "flex flex-col",
58
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
59
+ className: "text-sm",
60
+ children: date.fromNow()
61
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
62
+ className: "text-xs text-muted-foreground",
63
+ children: date.format("YYYY-MM-DD HH:mm:ss")
64
+ })]
65
+ });
66
+ }
67
+ function BundleChildrenPanel({ panelId, bundle, bundles, loading, onDetailClick }) {
68
+ const isMobile = useIsMobile();
69
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
70
+ "aria-live": "polite",
71
+ className: "border-t bg-muted/10 p-3 sm:p-4",
72
+ id: panelId,
73
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
74
+ className: "flex flex-col gap-4",
75
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
76
+ className: "flex flex-wrap items-center justify-between gap-3",
77
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
78
+ className: "flex min-w-0 items-start gap-2 text-sm sm:items-center",
79
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
80
+ className: "text-muted-foreground",
81
+ children: "Base bundle"
82
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
83
+ bundleId: bundle.id,
84
+ fullOnMobile: true,
85
+ maxLength: 18
86
+ })]
87
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
88
+ variant: "outline",
89
+ children: [
90
+ bundles.length,
91
+ " ",
92
+ bundles.length === 1 ? "patch" : "patches"
93
+ ]
94
+ })]
95
+ }), loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
96
+ className: "flex flex-col gap-2",
97
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" })]
98
+ }) : bundles.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
99
+ className: "flex flex-col gap-2",
100
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
101
+ className: "text-xs font-semibold uppercase text-muted-foreground/70",
102
+ children: "Patch bundles from this base"
103
+ }), isMobile ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
104
+ className: "flex flex-col gap-2",
105
+ children: bundles.map((childBundle) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
106
+ className: "rounded-md border bg-background p-3",
107
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
108
+ className: "flex flex-col gap-3",
109
+ children: [
110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
111
+ className: "space-y-1",
112
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
113
+ className: "text-[11px] font-medium uppercase text-muted-foreground/70",
114
+ children: "Patch Bundle"
115
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
116
+ bundleId: childBundle.id,
117
+ fullOnMobile: true,
118
+ maxLength: 18
119
+ })]
120
+ }),
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
122
+ className: "space-y-1",
123
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
124
+ className: "text-[11px] font-medium uppercase text-muted-foreground/70",
125
+ children: "Relation"
126
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127
+ className: "flex flex-col items-start gap-1 text-sm",
128
+ children: [
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
130
+ bundleId: bundle.id,
131
+ fullOnMobile: true,
132
+ maxLength: 12
133
+ }),
134
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowRight, { className: "size-4 rotate-90 text-muted-foreground" }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
136
+ bundleId: childBundle.id,
137
+ fullOnMobile: true,
138
+ maxLength: 12
139
+ })
140
+ ]
141
+ })]
142
+ }),
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
144
+ className: "flex items-center justify-between gap-3",
145
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
146
+ className: "space-y-1",
147
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
148
+ className: "text-[11px] font-medium uppercase text-muted-foreground/70",
149
+ children: "Artifact"
150
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
151
+ variant: "secondary",
152
+ children: "bsdiff"
153
+ })]
154
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
155
+ className: "space-y-1 text-right",
156
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
157
+ className: "text-[11px] font-medium uppercase text-muted-foreground/70",
158
+ children: "Created"
159
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
160
+ className: "text-xs tabular-nums text-foreground",
161
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TimestampDisplay, { uuid: childBundle.id })
162
+ })]
163
+ })]
164
+ }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
166
+ className: "w-full",
167
+ onClick: () => onDetailClick(childBundle),
168
+ size: "sm",
169
+ type: "button",
170
+ variant: "outline",
171
+ children: "Detail"
172
+ })
173
+ ]
174
+ })
175
+ }, childBundle.id))
176
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
177
+ className: "overflow-x-auto rounded-md border bg-background",
178
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Table, {
179
+ className: "min-w-max",
180
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, {
181
+ className: "hover:bg-transparent",
182
+ children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Patch Bundle" }),
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Relation" }),
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Artifact" }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Created" }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, {
188
+ className: "w-24 text-right",
189
+ children: "Detail"
190
+ })
191
+ ]
192
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableBody, { children: bundles.map((childBundle) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, { children: [
193
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
194
+ bundleId: childBundle.id,
195
+ fullOnMobile: true,
196
+ maxLength: 18
197
+ }) }),
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
199
+ className: "flex min-w-[280px] flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2",
200
+ children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
202
+ bundleId: bundle.id,
203
+ fullOnMobile: true,
204
+ maxLength: 12
205
+ }),
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowRight, { className: "size-4 shrink-0 rotate-90 text-muted-foreground sm:rotate-0" }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
208
+ bundleId: childBundle.id,
209
+ fullOnMobile: true,
210
+ maxLength: 12
211
+ })
212
+ ]
213
+ }) }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
215
+ variant: "secondary",
216
+ children: "bsdiff"
217
+ }) }),
218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
219
+ className: "tabular-nums",
220
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TimestampDisplay, { uuid: childBundle.id })
221
+ }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
223
+ className: "text-right",
224
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
225
+ onClick: () => onDetailClick(childBundle),
226
+ size: "sm",
227
+ type: "button",
228
+ variant: "outline",
229
+ children: "Detail"
230
+ })
231
+ })
232
+ ] }, childBundle.id)) })]
233
+ })
234
+ })]
235
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
236
+ className: "flex flex-col gap-2",
237
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
238
+ className: "text-xs font-semibold uppercase text-muted-foreground/70",
239
+ children: "Patch bundles from this base"
240
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
241
+ className: "rounded-md border bg-background p-3 text-sm text-muted-foreground",
242
+ children: "No direct patch bundles."
243
+ })]
244
+ })]
245
+ })
246
+ });
247
+ }
248
+ function ChannelManagementDialog({ open, onOpenChange }) {
249
+ const [channelName, setChannelName] = (0, import_react.useState)("");
250
+ const [channelToDelete, setChannelToDelete] = (0, import_react.useState)(null);
251
+ const { data: channels = [], isPending: isLoadingChannels } = useChannelsQuery();
252
+ const createChannel = useCreateChannelMutation();
253
+ const deleteChannel = useDeleteChannelMutation();
254
+ const normalizedName = channelName.trim();
255
+ const handleCreate = async (event) => {
256
+ event.preventDefault();
257
+ if (!normalizedName || createChannel.isPending) return;
258
+ try {
259
+ const result = await createChannel.mutateAsync({
260
+ row: {
261
+ id: createUUIDv7(),
262
+ name: normalizedName
263
+ },
264
+ onConflict: "returnExisting"
265
+ });
266
+ setChannelName("");
267
+ if (result.inserted) toast.success(`Channel ${result.row.name} created`);
268
+ else toast.info(`Channel ${result.row.name} already exists`);
269
+ } catch (error) {
270
+ toast.error("Failed to create channel");
271
+ console.error(error);
272
+ }
273
+ };
274
+ const handleDelete = async () => {
275
+ if (!channelToDelete || deleteChannel.isPending) return;
276
+ try {
277
+ const result = await deleteChannel.mutateAsync({ id: channelToDelete.id });
278
+ if (result.deleted) toast.success(`Channel ${channelToDelete.name} deleted`);
279
+ else if (result.reason === "not_empty") toast.error(`Channel ${channelToDelete.name} is still in use and cannot be deleted`);
280
+ else toast.error(`Channel ${channelToDelete.name} no longer exists`);
281
+ setChannelToDelete(null);
282
+ } catch (error) {
283
+ toast.error("Failed to delete channel");
284
+ console.error(error);
285
+ }
286
+ };
287
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
288
+ open,
289
+ onOpenChange,
290
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogContent, {
291
+ className: "sm:max-w-lg",
292
+ children: [
293
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogTitle$1, {
294
+ className: "flex items-center gap-2",
295
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { className: "size-4 text-muted-foreground" }), "Channels"]
296
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription$1, { children: "Create deployment channels and remove them when no Release uses them. Empty channels remain available until you delete them." })] }),
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", {
298
+ onSubmit: handleCreate,
299
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldGroup, {
300
+ className: "gap-2",
301
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
302
+ orientation: "horizontal",
303
+ children: [
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
305
+ htmlFor: "channel-name",
306
+ className: "sr-only",
307
+ children: "Channel name"
308
+ }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
310
+ id: "channel-name",
311
+ value: channelName,
312
+ onChange: (event) => setChannelName(event.target.value),
313
+ placeholder: "Channel name",
314
+ autoComplete: "off",
315
+ disabled: createChannel.isPending
316
+ }),
317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
318
+ type: "submit",
319
+ disabled: !normalizedName || createChannel.isPending,
320
+ children: [createChannel.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, {
321
+ "data-icon": "inline-start",
322
+ className: "animate-spin"
323
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "Create"]
324
+ })
325
+ ]
326
+ })
327
+ })
328
+ }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
330
+ className: "max-h-72 overflow-y-auto rounded-lg border",
331
+ children: isLoadingChannels ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
332
+ className: "flex h-20 items-center justify-center text-muted-foreground",
333
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "mr-2 size-3.5 animate-spin" }), "Loading channels"]
334
+ }) : channels.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
335
+ className: "flex h-20 items-center justify-center text-muted-foreground",
336
+ children: "No channels yet"
337
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
338
+ className: "divide-y",
339
+ children: channels.map((channel) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
340
+ className: "flex min-h-12 items-center gap-3 px-3 py-2",
341
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
342
+ className: "min-w-0 flex-1",
343
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
344
+ className: "truncate text-xs font-medium",
345
+ children: channel.name
346
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
347
+ className: "truncate font-mono text-[10px] text-muted-foreground",
348
+ children: channel.id
349
+ })]
350
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
351
+ type: "button",
352
+ variant: "ghost",
353
+ size: "icon-sm",
354
+ "aria-label": `Delete ${channel.name}`,
355
+ title: `Delete ${channel.name}`,
356
+ disabled: deleteChannel.isPending,
357
+ onClick: () => setChannelToDelete(channel),
358
+ className: "text-muted-foreground hover:text-destructive",
359
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, { "data-icon": "inline-start" })
360
+ })]
361
+ }, channel.id))
362
+ })
363
+ })
364
+ ]
365
+ })
366
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialog$1, {
367
+ open: channelToDelete !== null,
368
+ onOpenChange: (nextOpen) => {
369
+ if (!nextOpen && !deleteChannel.isPending) setChannelToDelete(null);
370
+ },
371
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogTitle, { children: [
372
+ "Delete ",
373
+ channelToDelete?.name,
374
+ "?"
375
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogDescription, { children: "Only empty channels can be removed. The server checks this when you delete it, and the channel can be created again later with a new ID." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogCancel, {
376
+ disabled: deleteChannel.isPending,
377
+ children: "Cancel"
378
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogAction, {
379
+ variant: "destructive",
380
+ disabled: deleteChannel.isPending,
381
+ onClick: (event) => {
382
+ event.preventDefault();
383
+ handleDelete();
384
+ },
385
+ children: deleteChannel.isPending ? "Deleting..." : "Delete channel"
386
+ })] })] })
387
+ })] });
388
+ }
389
+ function Metric({ label, tone, value }) {
390
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
391
+ className: "flex flex-col gap-1",
392
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dt", {
393
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
394
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
395
+ "aria-hidden": "true",
396
+ className: tone === "applied" ? "size-2 rounded-full bg-chart-2" : "size-2 rounded-full bg-muted-foreground"
397
+ }), label]
398
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
399
+ className: "text-xl font-semibold tracking-tight tabular-nums",
400
+ children: value
401
+ })]
402
+ });
403
+ }
404
+ function SupportedBundleAnalyticsSummary({ bundleId }) {
405
+ const { data, error, isLoading } = useBundleEventAnalyticsQuery({
406
+ bundleId,
407
+ window: "30d",
408
+ limit: 1,
409
+ offset: 0
410
+ }, true);
411
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, {
412
+ className: "p-4 pb-3",
413
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
414
+ className: "text-sm font-medium",
415
+ children: "Activity · 30 days"
416
+ })
417
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
418
+ className: "flex flex-col gap-3 px-4 pb-4",
419
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
420
+ "aria-label": "Loading reported bundle outcomes",
421
+ className: "flex flex-col gap-3",
422
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
423
+ className: "grid grid-cols-2 gap-4",
424
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" })]
425
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-24 w-full sm:h-32" })]
426
+ }) : error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsErrorAlert, {
427
+ error: error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to load bundle analytics."),
428
+ fallbackTitle: "Analytics unavailable"
429
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
430
+ className: "grid grid-cols-2 divide-x divide-border/70",
431
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
432
+ className: "pr-4",
433
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
434
+ label: "Applied",
435
+ tone: "applied",
436
+ value: data?.summary.installed ?? 0
437
+ })
438
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
439
+ className: "pl-4",
440
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
441
+ label: "Recovered",
442
+ tone: "recovered",
443
+ value: data?.summary.recovered ?? 0
444
+ })
445
+ })]
446
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleActivityChart, {
447
+ installed: data?.series.installed ?? [],
448
+ recovered: data?.series.recovered ?? [],
449
+ window: "30d"
450
+ })] })
451
+ })] });
452
+ }
453
+ function BundleAnalyticsSummary({ bundleId }) {
454
+ if (useAnalyticsCapability().status !== "supported") return null;
455
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SupportedBundleAnalyticsSummary, { bundleId });
456
+ }
457
+ function HashValueDisplay({ value, maxLength = 12, className }) {
458
+ const isTruncated = value.length > maxLength;
459
+ const truncated = isTruncated ? value.slice(0, maxLength) : value;
460
+ const copyValue = async () => {
461
+ try {
462
+ await navigator.clipboard.writeText(value);
463
+ toast.success("Copied to clipboard", { description: value });
464
+ } catch {
465
+ toast.error("Failed to copy value");
466
+ }
467
+ };
468
+ const handleClick = (event) => {
469
+ event.preventDefault();
470
+ event.stopPropagation();
471
+ copyValue();
472
+ };
473
+ const handleKeyDown = (event) => {
474
+ if (event.key === "Enter" || event.key === " ") {
475
+ event.preventDefault();
476
+ event.stopPropagation();
477
+ copyValue();
478
+ }
479
+ };
480
+ const content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
481
+ translate: "no",
482
+ className: cn("font-mono text-xs tabular-nums whitespace-nowrap", className),
483
+ children: truncated
484
+ });
485
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipTrigger, {
486
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
487
+ type: "button",
488
+ translate: "no",
489
+ className: cn("ring-ring/30 bg-muted/40 border-border/70 inline-flex min-w-0 max-w-full cursor-pointer items-center rounded-md border px-1.5 py-0.5 align-top shadow-xs outline-none transition-[background-color,border-color,transform,box-shadow]", "hover:bg-muted/70 active:scale-[0.98] active:bg-muted/85 focus-visible:ring-[2px]", "touch-manipulation select-none", !isTruncated && "cursor-copy"),
490
+ onClick: handleClick,
491
+ onKeyDown: handleKeyDown
492
+ }),
493
+ children: content
494
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipContent, {
495
+ hidden: !isTruncated,
496
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
497
+ translate: "no",
498
+ className: "break-all font-mono text-xs tabular-nums",
499
+ children: value
500
+ })
501
+ })] });
502
+ }
503
+ var normalizeGitUrl = (gitUrl) => {
504
+ const trimmedGitUrl = gitUrl.trim();
505
+ if (trimmedGitUrl.startsWith("git@")) {
506
+ const sshMatch = trimmedGitUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
507
+ if (sshMatch) return `https://${sshMatch[1]}/${sshMatch[2]}`;
508
+ }
509
+ if (trimmedGitUrl.startsWith("ssh://")) try {
510
+ const url = new URL(trimmedGitUrl);
511
+ return `https://${url.hostname}${url.pathname}`.replace(/\/+$/, "").replace(/\.git$/, "");
512
+ } catch {
513
+ return trimmedGitUrl.replace(/\/+$/, "").replace(/\.git$/, "");
514
+ }
515
+ return trimmedGitUrl.replace(/\/+$/, "").replace(/\.git$/, "");
516
+ };
517
+ var getCommitUrl = (gitUrl, commitHash) => {
518
+ if (!gitUrl?.trim()) return null;
519
+ const normalizedGitUrl = normalizeGitUrl(gitUrl);
520
+ try {
521
+ const { hostname } = new URL(normalizedGitUrl);
522
+ if (hostname.includes("gitlab")) return `${normalizedGitUrl}/-/commit/${commitHash}`;
523
+ if (hostname.includes("bitbucket")) return `${normalizedGitUrl}/commits/${commitHash}`;
524
+ } catch {}
525
+ return `${normalizedGitUrl}/commit/${commitHash}`;
526
+ };
527
+ function Row({ label, value }) {
528
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
529
+ className: "flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
530
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
531
+ className: "text-muted-foreground",
532
+ children: label
533
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
534
+ className: "min-w-0 text-left text-sm sm:text-right",
535
+ children: value
536
+ })]
537
+ });
538
+ }
539
+ function BundleMetadata({ bundle, release }) {
540
+ const { data: configData, isFetched } = useConfigQuery();
541
+ const patchBaseBundleId = bundle ? getPatchBaseBundleId(bundle) : null;
542
+ const hbcPatchFileHash = bundle ? getPatchFileHash(bundle) : null;
543
+ const hbcPatchBaseFileHash = bundle ? getPatchBaseFileHash(bundle) : null;
544
+ const gitCommitUrl = bundle?.gitCommitHash && isFetched ? getCommitUrl(configData?.console.gitUrl, bundle.gitCommitHash) : null;
545
+ const target = release.strategy === "APP_VERSION" ? release.target_app_version : release.fingerprint_hash;
546
+ if (!(target || bundle?.gitCommitHash || bundle?.fileHash || patchBaseBundleId || hbcPatchBaseFileHash || hbcPatchFileHash)) return null;
547
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, {
548
+ className: "p-4 pb-3",
549
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
550
+ className: "text-sm font-medium",
551
+ children: "Metadata"
552
+ })
553
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
554
+ className: "px-4 pb-4",
555
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
556
+ className: "flex flex-col gap-3 text-sm",
557
+ children: [
558
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
559
+ label: release.strategy === "APP_VERSION" ? "Target app version" : "Fingerprint target",
560
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
561
+ className: "break-all font-mono text-xs",
562
+ translate: "no",
563
+ children: target ?? "—"
564
+ })
565
+ }),
566
+ bundle?.gitCommitHash ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
567
+ label: "Git commit",
568
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
569
+ className: "flex items-center justify-start gap-1 sm:justify-end",
570
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(HashValueDisplay, {
571
+ className: gitCommitUrl ? "text-primary" : void 0,
572
+ maxLength: 12,
573
+ value: bundle.gitCommitHash
574
+ }), gitCommitUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
575
+ "aria-label": "Open git commit",
576
+ className: "shrink-0 text-primary hover:underline",
577
+ href: gitCommitUrl,
578
+ rel: "noopener noreferrer",
579
+ target: "_blank",
580
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExternalLink, { "aria-hidden": "true" })
581
+ }) : null]
582
+ })
583
+ }) : null,
584
+ bundle?.fileHash ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
585
+ label: "Bundle hash",
586
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HashValueDisplay, {
587
+ maxLength: 16,
588
+ value: bundle.fileHash
589
+ })
590
+ }) : null,
591
+ patchBaseBundleId ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
592
+ label: "Patch base",
593
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
594
+ bundleId: patchBaseBundleId,
595
+ fullOnMobile: true,
596
+ maxLength: 18
597
+ })
598
+ }) : null,
599
+ hbcPatchBaseFileHash ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
600
+ label: "Base hash",
601
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HashValueDisplay, {
602
+ maxLength: 16,
603
+ value: hbcPatchBaseFileHash
604
+ })
605
+ }) : null,
606
+ hbcPatchFileHash ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
607
+ label: "Patch hash",
608
+ value: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HashValueDisplay, {
609
+ maxLength: 16,
610
+ value: hbcPatchFileHash
611
+ })
612
+ }) : null
613
+ ]
614
+ })
615
+ })] });
616
+ }
617
+ var ALL_NUMERIC_COHORTS = Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
618
+ function RolloutCohortsDialog({ releaseId, rolloutCohortCount, targetCohorts }) {
619
+ const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
620
+ const normalizedTargetCohorts = targetCohorts ?? [];
621
+ const hasTargetCohorts = normalizedTargetCohorts.length > 0;
622
+ const isPartialRollout = normalizedRolloutCount > 0 && normalizedRolloutCount < 1e3;
623
+ const isFullRollout = normalizedRolloutCount === NUMERIC_COHORT_SIZE;
624
+ const isMobile = useIsMobile();
625
+ const rolloutCohorts = isFullRollout ? ALL_NUMERIC_COHORTS : isPartialRollout ? ALL_NUMERIC_COHORTS.filter((cohortValue) => getNumericCohortRolloutPosition(releaseId, cohortValue) < normalizedRolloutCount) : [];
626
+ const rolloutPercentage = (normalizedRolloutCount / 10).toFixed(1);
627
+ const selectedCount = isFullRollout ? NUMERIC_COHORT_SIZE : rolloutCohorts.length;
628
+ const excludedCount = NUMERIC_COHORT_SIZE - selectedCount;
629
+ const dialogBody = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
630
+ className: "flex flex-col gap-4",
631
+ children: [
632
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
633
+ className: "grid grid-cols-2 gap-3",
634
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
635
+ className: "p-4",
636
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDescription, { children: "Selected cohorts" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
637
+ className: "font-mono text-2xl tabular-nums",
638
+ children: selectedCount
639
+ })]
640
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
641
+ className: "p-4",
642
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDescription, { children: "Excluded cohorts" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
643
+ className: "font-mono text-2xl tabular-nums",
644
+ children: excludedCount
645
+ })]
646
+ }) })]
647
+ }),
648
+ hasTargetCohorts ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
649
+ className: "p-4 pb-3",
650
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
651
+ className: "text-sm",
652
+ children: "Additional cohorts"
653
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDescription, { children: "Always included in addition to the percentage rollout." })]
654
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
655
+ className: "p-4 pt-0",
656
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
657
+ className: "flex flex-wrap gap-2 rounded-lg border bg-muted/20 p-3",
658
+ children: normalizedTargetCohorts.map((cohort) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
659
+ className: "max-w-full font-mono",
660
+ variant: "secondary",
661
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
662
+ className: "truncate",
663
+ children: cohort
664
+ })
665
+ }, cohort))
666
+ })
667
+ })] }) : null,
668
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, {
669
+ className: "p-4 pb-3",
670
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
671
+ className: "text-sm",
672
+ children: "Numeric cohorts"
673
+ })
674
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
675
+ className: "p-4 pt-0",
676
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
677
+ className: "max-h-[50vh] overflow-y-auto overscroll-contain rounded-lg border bg-muted/20 p-3 sm:max-h-[45vh]",
678
+ children: rolloutCohorts.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
679
+ "aria-label": "Included numeric cohorts",
680
+ className: "grid grid-cols-3 gap-2 sm:grid-cols-6 lg:grid-cols-8",
681
+ role: "list",
682
+ children: rolloutCohorts.map((cohortValue) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
683
+ className: "justify-center font-mono tabular-nums",
684
+ role: "listitem",
685
+ variant: "outline",
686
+ children: cohortValue
687
+ }, cohortValue))
688
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
689
+ className: "text-sm text-muted-foreground",
690
+ children: "No numeric cohorts are included."
691
+ })
692
+ })
693
+ })] })
694
+ ]
695
+ });
696
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Dialog$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogTrigger, {
697
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
698
+ size: "sm",
699
+ type: "button",
700
+ variant: "outline"
701
+ }),
702
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(List, { "data-icon": "inline-start" }), "Preview cohorts"]
703
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogContent, {
704
+ className: isMobile ? "top-0 left-0 h-dvh max-w-none translate-x-0 translate-y-0 rounded-none border-0 p-0" : "sm:max-w-3xl",
705
+ children: isMobile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
706
+ className: "flex h-full flex-col overflow-hidden",
707
+ children: [
708
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, {
709
+ className: "shrink-0 border-b border-border/70 px-4 py-4",
710
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle$1, { children: "Rolled out cohorts" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogDescription$1, { children: [
711
+ rolloutPercentage,
712
+ "% includes ",
713
+ selectedCount,
714
+ " of",
715
+ " ",
716
+ NUMERIC_COHORT_SIZE,
717
+ " numeric cohorts.",
718
+ hasTargetCohorts ? " Additional cohorts are always included." : ""
719
+ ] })]
720
+ }),
721
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
722
+ className: "flex-1 overflow-y-auto overscroll-contain px-4 py-4",
723
+ children: dialogBody
724
+ }),
725
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogFooter, {
726
+ className: "shrink-0 border-t border-border/70 px-4 py-3",
727
+ showCloseButton: true
728
+ })
729
+ ]
730
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle$1, { children: "Rolled out cohorts" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogDescription$1, { children: [
732
+ rolloutPercentage,
733
+ "% includes ",
734
+ selectedCount,
735
+ " of",
736
+ " ",
737
+ NUMERIC_COHORT_SIZE,
738
+ " numeric cohorts.",
739
+ hasTargetCohorts ? " Additional cohorts are always included." : ""
740
+ ] })] }),
741
+ dialogBody,
742
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogFooter, { showCloseButton: true })
743
+ ] })
744
+ })] });
745
+ }
746
+ function AppleIcon({ className }) {
747
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
748
+ "aria-hidden": "true",
749
+ viewBox: "0 0 24 24",
750
+ xmlns: "http://www.w3.org/2000/svg",
751
+ className,
752
+ fill: "currentColor",
753
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" })
754
+ });
755
+ }
756
+ function AndroidIcon({ className }) {
757
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
758
+ "aria-hidden": "true",
759
+ viewBox: "0 0 24 24",
760
+ xmlns: "http://www.w3.org/2000/svg",
761
+ className,
762
+ fill: "currentColor",
763
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" })
764
+ });
765
+ }
766
+ function PlatformIcon({ platform, className }) {
767
+ if (platform === "ios") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppleIcon, { className });
768
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AndroidIcon, { className });
769
+ }
770
+ var Select$1 = SelectRoot;
771
+ function SelectGroup({ className, ...props }) {
772
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectGroup$1, {
773
+ "data-slot": "select-group",
774
+ className: cn("scroll-my-1 p-1", className),
775
+ ...props
776
+ });
777
+ }
778
+ function SelectValue({ className, ...props }) {
779
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue$1, {
780
+ "data-slot": "select-value",
781
+ className: cn("flex flex-1 text-left", className),
782
+ ...props
783
+ });
784
+ }
785
+ function SelectTrigger({ className, size = "default", children, ...props }) {
786
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectTrigger$1, {
787
+ "data-slot": "select-trigger",
788
+ "data-size": size,
789
+ className: cn("flex w-fit items-center justify-between gap-1.5 rounded-md border border-input bg-input/20 px-2 py-1.5 text-xs/relaxed whitespace-nowrap transition-colors outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", className),
790
+ ...props,
791
+ children: [children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectIcon, { render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })]
792
+ });
793
+ }
794
+ function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, ...props }) {
795
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectPositioner, {
796
+ side,
797
+ sideOffset,
798
+ align,
799
+ alignOffset,
800
+ alignItemWithTrigger,
801
+ className: "isolate z-50",
802
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectPopup, {
803
+ "data-slot": "select-content",
804
+ "data-align-trigger": alignItemWithTrigger,
805
+ className: cn("dark relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
806
+ ...props,
807
+ children: [
808
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollUpButton, {}),
809
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectList, { children }),
810
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollDownButton, {})
811
+ ]
812
+ })
813
+ }) });
814
+ }
815
+ function SelectItem({ className, children, ...props }) {
816
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectItem$1, {
817
+ "data-slot": "select-item",
818
+ className: cn("relative flex min-h-7 w-full cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
819
+ ...props,
820
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItemText, {
821
+ className: "flex flex-1 shrink-0 gap-2 whitespace-nowrap",
822
+ children
823
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItemIndicator, {
824
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center" }),
825
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "pointer-events-none" })
826
+ })]
827
+ });
828
+ }
829
+ function SelectScrollUpButton({ className, ...props }) {
830
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollUpArrow, {
831
+ "data-slot": "select-scroll-up-button",
832
+ className: cn("top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5", className),
833
+ ...props,
834
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, {})
835
+ });
836
+ }
837
+ function SelectScrollDownButton({ className, ...props }) {
838
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollDownArrow, {
839
+ "data-slot": "select-scroll-down-button",
840
+ className: cn("bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5", className),
841
+ ...props,
842
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, {})
843
+ });
844
+ }
845
+ function Sheet({ ...props }) {
846
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogRoot, {
847
+ "data-slot": "sheet",
848
+ ...props
849
+ });
850
+ }
851
+ function SheetPortal({ ...props }) {
852
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPortal, {
853
+ "data-slot": "sheet-portal",
854
+ ...props
855
+ });
856
+ }
857
+ function SheetOverlay({ className, ...props }) {
858
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogBackdrop, {
859
+ "data-slot": "sheet-overlay",
860
+ className: cn("fixed inset-0 z-50 bg-black/80 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs", className),
861
+ ...props
862
+ });
863
+ }
864
+ function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }) {
865
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SheetPortal, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SheetOverlay, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogPopup, {
866
+ "data-slot": "sheet-content",
867
+ "data-side": side,
868
+ className: cn("fixed z-50 flex flex-col bg-background bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className),
869
+ ...props,
870
+ children: [children, showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogClose, {
871
+ "data-slot": "sheet-close",
872
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
873
+ variant: "ghost",
874
+ className: "absolute top-4 right-4",
875
+ size: "icon-sm"
876
+ }),
877
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
878
+ className: "sr-only",
879
+ children: "Close"
880
+ })]
881
+ })]
882
+ })] });
883
+ }
884
+ function SheetHeader({ className, ...props }) {
885
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
886
+ "data-slot": "sheet-header",
887
+ className: cn("flex flex-col gap-1.5 p-6", className),
888
+ ...props
889
+ });
890
+ }
891
+ function SheetTitle({ className, ...props }) {
892
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, {
893
+ "data-slot": "sheet-title",
894
+ className: cn("text-sm font-medium text-foreground", className),
895
+ ...props
896
+ });
897
+ }
898
+ function SheetDescription({ className, ...props }) {
899
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription, {
900
+ "data-slot": "sheet-description",
901
+ className: cn("text-xs/relaxed text-muted-foreground", className),
902
+ ...props
903
+ });
904
+ }
905
+ function Slider$1({ className, defaultValue, value, min = 0, max = 100, ...props }) {
906
+ const _values = Array.isArray(value) ? value : value !== void 0 ? [value] : Array.isArray(defaultValue) ? defaultValue : defaultValue !== void 0 ? [defaultValue] : [min];
907
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SliderRoot, {
908
+ className: cn("data-horizontal:w-full data-vertical:h-full", className),
909
+ "data-slot": "slider",
910
+ defaultValue,
911
+ value,
912
+ min,
913
+ max,
914
+ thumbAlignment: "edge",
915
+ ...props,
916
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SliderControl, {
917
+ className: "relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col",
918
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SliderTrack, {
919
+ "data-slot": "slider-track",
920
+ className: "relative grow overflow-hidden rounded-full bg-primary/20 select-none data-horizontal:h-1.5 data-horizontal:w-full data-vertical:h-full data-vertical:w-1.5",
921
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SliderIndicator, {
922
+ "data-slot": "slider-range",
923
+ className: "bg-primary select-none data-horizontal:h-full data-vertical:w-full"
924
+ })
925
+ }), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SliderThumb, {
926
+ "data-slot": "slider-thumb",
927
+ className: "relative block size-4 shrink-0 rounded-full border border-primary/50 bg-background shadow transition-colors select-none after:absolute after:-inset-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:opacity-50"
928
+ }, index))]
929
+ })
930
+ });
931
+ }
932
+ function Switch$1({ className, ...props }) {
933
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SwitchRoot, {
934
+ "data-slot": "switch",
935
+ className: cn("peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:cursor-not-allowed data-disabled:opacity-50", className),
936
+ ...props,
937
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SwitchThumb, {
938
+ "data-slot": "switch-thumb",
939
+ className: "pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-checked:translate-x-4 data-unchecked:translate-x-0"
940
+ })
941
+ });
942
+ }
943
+ var promoteActionItems = [{
944
+ label: "Move bundle",
945
+ value: "move"
946
+ }, {
947
+ label: "Copy bundle",
948
+ value: "copy"
949
+ }];
950
+ var draftFromRelease = (release) => ({
951
+ enabled: release.enabled,
952
+ message: release.message ?? "",
953
+ rolloutCohortCount: release.rollout_cohort_count,
954
+ shouldForceUpdate: release.should_force_update,
955
+ targetAppVersion: release.target_app_version ?? "",
956
+ targetCohorts: [...release.target_cohorts]
957
+ });
958
+ var patchFromDraft = (release, draft) => ({
959
+ enabled: draft.enabled,
960
+ message: draft.message.trim() || null,
961
+ rolloutCohortCount: draft.rolloutCohortCount,
962
+ shouldForceUpdate: draft.shouldForceUpdate,
963
+ ...release.strategy === "APP_VERSION" ? { targetAppVersion: draft.targetAppVersion.trim() } : {},
964
+ targetCohorts: draft.targetCohorts
965
+ });
966
+ var formatRolloutPercentage = (count) => (count / 10).toFixed(1);
967
+ var formatRolloutInput = (value) => {
968
+ const normalized = value.replace(",", ".");
969
+ const [integer = "", ...decimals] = normalized.split(".");
970
+ const integerDigits = integer.replace(/\D/g, "").slice(0, 3);
971
+ const decimalDigit = decimals.join("").replace(/\D/g, "").slice(0, 1);
972
+ return normalized.includes(".") ? `${integerDigits}.${decimalDigit}` : integerDigits;
973
+ };
974
+ var parseRolloutInput = (value) => {
975
+ const percentage = Number.parseFloat(value);
976
+ if (!Number.isFinite(percentage)) return null;
977
+ return Math.min(1e3, Math.max(0, Math.trunc(percentage * 10)));
978
+ };
979
+ function RolloutPercentageInput({ value, onChange }) {
980
+ const [inputValue, setInputValue] = (0, import_react.useState)(formatRolloutPercentage(value));
981
+ const [focused, setFocused] = (0, import_react.useState)(false);
982
+ (0, import_react.useEffect)(() => {
983
+ if (!focused) setInputValue(formatRolloutPercentage(value));
984
+ }, [focused, value]);
985
+ const commitInput = () => {
986
+ const nextValue = parseRolloutInput(inputValue) ?? value;
987
+ setFocused(false);
988
+ setInputValue(formatRolloutPercentage(nextValue));
989
+ onChange(nextValue);
990
+ };
991
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
992
+ className: "flex items-center gap-3",
993
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slider$1, {
994
+ "aria-label": "Rollout percentage",
995
+ max: 1e3,
996
+ min: 0,
997
+ onValueChange: (nextValue) => onChange(Array.isArray(nextValue) ? nextValue[0] ?? value : nextValue),
998
+ step: 1,
999
+ value
1000
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputGroup, {
1001
+ className: "w-24 shrink-0",
1002
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupInput, {
1003
+ "aria-label": "Rollout percentage",
1004
+ autoComplete: "off",
1005
+ className: "text-right tabular-nums",
1006
+ id: "release-rollout-percentage",
1007
+ inputMode: "decimal",
1008
+ name: "rolloutPercentage",
1009
+ onBlur: commitInput,
1010
+ onChange: (event) => {
1011
+ const nextInput = formatRolloutInput(event.target.value);
1012
+ setInputValue(nextInput);
1013
+ const parsed = parseRolloutInput(nextInput);
1014
+ if (parsed !== null) onChange(parsed);
1015
+ },
1016
+ onFocus: () => setFocused(true),
1017
+ value: inputValue
1018
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupAddon, {
1019
+ align: "inline-end",
1020
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupText, { children: "%" })
1021
+ })]
1022
+ })]
1023
+ });
1024
+ }
1025
+ function ReleaseEditorSheet({ channels, onOpenChange, open, releaseId }) {
1026
+ const releaseQuery = useReleaseQuery(releaseId);
1027
+ const release = releaseQuery.data;
1028
+ const bundleQuery = useBundleQuery(release?.bundle_id ?? "");
1029
+ const bundle = bundleQuery.data;
1030
+ const diagnostics = useReleaseCatalogDiagnosticsQuery(release?.scope_key ?? "");
1031
+ const update = useUpdateReleaseMutation();
1032
+ const preflight = usePreflightReleaseMutation();
1033
+ const remove = useDeleteReleaseMutation();
1034
+ const promote = usePromoteReleaseMutation();
1035
+ const [draft, setDraft] = (0, import_react.useState)(null);
1036
+ const [newCohort, setNewCohort] = (0, import_react.useState)("");
1037
+ const [error, setError] = (0, import_react.useState)("");
1038
+ const [confirmDiscard, setConfirmDiscard] = (0, import_react.useState)(false);
1039
+ const [confirmDelete, setConfirmDelete] = (0, import_react.useState)(false);
1040
+ const [showPromote, setShowPromote] = (0, import_react.useState)(false);
1041
+ const [targetChannel, setTargetChannel] = (0, import_react.useState)("");
1042
+ const [promoteAction, setPromoteAction] = (0, import_react.useState)("move");
1043
+ (0, import_react.useEffect)(() => {
1044
+ setDraft(release ? draftFromRelease(release) : null);
1045
+ setError("");
1046
+ }, [release]);
1047
+ const initialDraft = release ? draftFromRelease(release) : null;
1048
+ const dirty = draft !== null && initialDraft !== null && JSON.stringify(draft) !== JSON.stringify(initialDraft);
1049
+ const busy = preflight.isPending || update.isPending || remove.isPending || promote.isPending;
1050
+ const requestClose = () => {
1051
+ if (busy) return;
1052
+ if (dirty) {
1053
+ setConfirmDiscard(true);
1054
+ return;
1055
+ }
1056
+ onOpenChange(false);
1057
+ };
1058
+ const addCohort = () => {
1059
+ if (!draft) return;
1060
+ const cohort = normalizeCohortValue(newCohort);
1061
+ if (!isValidCohort(cohort)) {
1062
+ setError(INVALID_COHORT_ERROR_MESSAGE);
1063
+ return;
1064
+ }
1065
+ setDraft({
1066
+ ...draft,
1067
+ targetCohorts: Array.from(new Set([...draft.targetCohorts, cohort]))
1068
+ });
1069
+ setNewCohort("");
1070
+ setError("");
1071
+ };
1072
+ const save = async () => {
1073
+ if (!release || !draft) return;
1074
+ const cohorts = draft.targetCohorts.map(normalizeCohortValue);
1075
+ if (cohorts.some((cohort) => !isValidCohort(cohort))) {
1076
+ setError(INVALID_COHORT_ERROR_MESSAGE);
1077
+ return;
1078
+ }
1079
+ if (release.strategy === "APP_VERSION" && !normalizeRange(draft.targetAppVersion.trim())) {
1080
+ setError("Enter a valid target app version range.");
1081
+ return;
1082
+ }
1083
+ const input = {
1084
+ expectedRevision: release.revision,
1085
+ patch: patchFromDraft(release, {
1086
+ ...draft,
1087
+ targetCohorts: cohorts
1088
+ }),
1089
+ releaseId: release.id
1090
+ };
1091
+ setError("");
1092
+ try {
1093
+ await preflight.mutateAsync(input);
1094
+ await update.mutateAsync(input);
1095
+ toast.success("Bundle settings saved");
1096
+ } catch (caught) {
1097
+ setError(caught instanceof Error ? caught.message : "Bundle settings could not be saved.");
1098
+ }
1099
+ };
1100
+ const channelName = channels.find((channel) => channel.id === release?.channel_id)?.name ?? release?.channel_id;
1101
+ const availableChannels = channels.filter((channel) => channel.id !== release?.channel_id);
1102
+ const availableChannelItems = availableChannels.map((channel) => ({
1103
+ label: channel.name,
1104
+ value: channel.name
1105
+ }));
1106
+ const normalizedTargetChannel = targetChannel.trim();
1107
+ const isCopyPromotion = promoteAction === "copy";
1108
+ const normalizedTargetAppVersion = draft ? normalizeRange(draft.targetAppVersion.trim()) : null;
1109
+ const resetPromoteDialog = () => {
1110
+ setTargetChannel("");
1111
+ setPromoteAction("move");
1112
+ };
1113
+ const handlePromoteOpenChange = (nextOpen) => {
1114
+ if (!nextOpen && promote.isPending) return;
1115
+ setShowPromote(nextOpen);
1116
+ if (!nextOpen) resetPromoteDialog();
1117
+ };
1118
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sheet, {
1120
+ onOpenChange: (nextOpen) => nextOpen ? void 0 : requestClose(),
1121
+ open,
1122
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SheetContent, {
1123
+ className: "min-w-0 overflow-hidden data-[side=right]:w-full sm:max-w-[600px]",
1124
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SheetHeader, {
1125
+ className: "shrink-0 pr-12",
1126
+ children: [
1127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SheetTitle, { children: "Bundle Detail" }),
1128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SheetDescription, {
1129
+ className: "sr-only",
1130
+ children: "Edit bundle delivery settings."
1131
+ }),
1132
+ release ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1133
+ className: "mt-1 flex min-w-0 flex-col gap-3 text-sm",
1134
+ children: [
1135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1136
+ className: "flex flex-wrap items-center gap-2",
1137
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1138
+ className: "flex items-center gap-2",
1139
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
1140
+ className: "size-4",
1141
+ platform: release.platform
1142
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1143
+ className: "font-medium",
1144
+ children: release.platform === "ios" ? "iOS" : "Android"
1145
+ })]
1146
+ })
1147
+ }),
1148
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1149
+ className: "flex flex-wrap items-start gap-2",
1150
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1151
+ className: "font-medium text-muted-foreground",
1152
+ children: "Bundle"
1153
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1154
+ className: "min-w-0 basis-full sm:basis-auto",
1155
+ children: release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
1156
+ bundleId: release.bundle_id,
1157
+ fullOnMobile: true,
1158
+ maxLength: 18
1159
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1160
+ className: "text-xs text-foreground",
1161
+ children: "Built-in app"
1162
+ })
1163
+ })]
1164
+ }),
1165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1166
+ className: "flex flex-wrap items-center gap-2",
1167
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1168
+ className: "font-medium text-muted-foreground",
1169
+ children: "Channel"
1170
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1171
+ className: "text-xs text-foreground",
1172
+ translate: "no",
1173
+ children: channelName
1174
+ })]
1175
+ }),
1176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1177
+ className: "flex flex-wrap items-center gap-2",
1178
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1179
+ className: "font-medium text-muted-foreground",
1180
+ children: "Platform"
1181
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1182
+ className: "text-xs text-foreground",
1183
+ children: release.platform === "ios" ? "iOS" : "Android"
1184
+ })]
1185
+ })
1186
+ ]
1187
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-12 w-full" })
1188
+ ]
1189
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1190
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
1191
+ children: [releaseQuery.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
1192
+ className: "m-4 sm:m-6",
1193
+ variant: "destructive",
1194
+ children: [
1195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
1196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Bundle details could not be loaded" }),
1197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: releaseQuery.error.message })
1198
+ ]
1199
+ }) : null, release && draft ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1200
+ className: "flex flex-col gap-6 px-4 pb-4 sm:px-6 sm:pb-6",
1201
+ children: [
1202
+ release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleAnalyticsSummary, { bundleId: release.bundle_id }) : null,
1203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
1204
+ "aria-labelledby": "delivery-settings-heading",
1205
+ className: "flex flex-col gap-4",
1206
+ children: [
1207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
1208
+ className: "text-sm font-medium",
1209
+ id: "delivery-settings-heading",
1210
+ children: "Delivery settings"
1211
+ }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(FieldGroup, {
1213
+ className: "gap-6",
1214
+ children: [
1215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1216
+ htmlFor: "release-message",
1217
+ children: "Message"
1218
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
1219
+ autoComplete: "off",
1220
+ id: "release-message",
1221
+ name: "message",
1222
+ onChange: (event) => setDraft({
1223
+ ...draft,
1224
+ message: event.target.value
1225
+ }),
1226
+ rows: 3,
1227
+ value: draft.message
1228
+ })] }),
1229
+ release.strategy === "APP_VERSION" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [
1230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1231
+ htmlFor: "release-target",
1232
+ children: "Target app version"
1233
+ }),
1234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1235
+ autoComplete: "off",
1236
+ id: "release-target",
1237
+ name: "targetAppVersion",
1238
+ onChange: (event) => setDraft({
1239
+ ...draft,
1240
+ targetAppVersion: event.target.value
1241
+ }),
1242
+ value: draft.targetAppVersion
1243
+ }),
1244
+ normalizedTargetAppVersion ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: normalizedTargetAppVersion }) : null
1245
+ ] }) : null,
1246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1247
+ className: "flex items-center justify-between gap-4",
1248
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1249
+ htmlFor: "release-enabled",
1250
+ children: "Enabled"
1251
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
1252
+ "aria-label": "Enabled",
1253
+ checked: draft.enabled,
1254
+ id: "release-enabled",
1255
+ name: "enabled",
1256
+ onCheckedChange: (enabled) => setDraft({
1257
+ ...draft,
1258
+ enabled
1259
+ })
1260
+ })]
1261
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: "Disabling rolls devices back to the previous enabled Release on their next check, or to the built-in app when none remains." })] }),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
1263
+ orientation: "horizontal",
1264
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1265
+ htmlFor: "release-force-update",
1266
+ children: "Force update"
1267
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
1268
+ "aria-label": "Force update",
1269
+ checked: draft.shouldForceUpdate,
1270
+ id: "release-force-update",
1271
+ name: "shouldForceUpdate",
1272
+ onCheckedChange: (shouldForceUpdate) => setDraft({
1273
+ ...draft,
1274
+ shouldForceUpdate
1275
+ })
1276
+ })]
1277
+ }),
1278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1279
+ htmlFor: "release-rollout-percentage",
1280
+ children: "Rollout percentage"
1281
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RolloutPercentageInput, {
1282
+ onChange: (rolloutCohortCount) => setDraft({
1283
+ ...draft,
1284
+ rolloutCohortCount
1285
+ }),
1286
+ value: draft.rolloutCohortCount
1287
+ })] }),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [
1289
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1290
+ className: "flex flex-wrap items-center justify-between gap-2",
1291
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1292
+ htmlFor: "release-cohort",
1293
+ children: "Additional cohorts (optional)"
1294
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RolloutCohortsDialog, {
1295
+ releaseId: release.id,
1296
+ rolloutCohortCount: draft.rolloutCohortCount,
1297
+ targetCohorts: draft.targetCohorts
1298
+ })]
1299
+ }),
1300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1301
+ className: "flex gap-2",
1302
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1303
+ autoComplete: "off",
1304
+ id: "release-cohort",
1305
+ name: "targetCohort",
1306
+ onChange: (event) => setNewCohort(event.target.value),
1307
+ onKeyDown: (event) => {
1308
+ if (event.key === "Enter") {
1309
+ event.preventDefault();
1310
+ addCohort();
1311
+ }
1312
+ },
1313
+ placeholder: "e.g. qa-team…",
1314
+ value: newCohort
1315
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1316
+ "aria-label": "Add cohort",
1317
+ disabled: !newCohort.trim(),
1318
+ onClick: addCohort,
1319
+ size: "icon",
1320
+ type: "button",
1321
+ variant: "outline",
1322
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, {})
1323
+ })]
1324
+ }),
1325
+ draft.targetCohorts.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1326
+ className: "flex flex-wrap gap-2",
1327
+ children: draft.targetCohorts.map((cohort) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
1328
+ className: "gap-1 font-mono",
1329
+ variant: "secondary",
1330
+ children: [cohort, /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
1331
+ "aria-label": `Remove cohort ${cohort}`,
1332
+ className: "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1333
+ onClick: () => setDraft({
1334
+ ...draft,
1335
+ targetCohorts: draft.targetCohorts.filter((value) => value !== cohort)
1336
+ }),
1337
+ type: "button",
1338
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { className: "size-3" })
1339
+ })]
1340
+ }, cohort))
1341
+ }) : null,
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: "Public names—no IDs or secrets." })
1343
+ ] })
1344
+ ]
1345
+ }),
1346
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: error }) : null,
1347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1348
+ className: "w-full",
1349
+ disabled: !dirty || busy,
1350
+ onClick: save,
1351
+ size: "lg",
1352
+ children: preflight.isPending || update.isPending ? "Saving…" : "Save changes"
1353
+ })
1354
+ ]
1355
+ }),
1356
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Separator$1, { className: "my-2" }),
1357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
1358
+ className: "flex flex-col gap-4",
1359
+ children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
1361
+ className: "text-sm font-medium",
1362
+ children: "Actions"
1363
+ }),
1364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1365
+ className: "w-full",
1366
+ disabled: release.kind !== "BUNDLE" || busy,
1367
+ onClick: () => setShowPromote(true),
1368
+ size: "sm",
1369
+ variant: "outline",
1370
+ children: "Promote to channel"
1371
+ }),
1372
+ release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1373
+ className: "w-full",
1374
+ disabled: busy,
1375
+ onClick: () => {
1376
+ window.open(`/api/bundles/${encodeURIComponent(release.bundle_id)}/download`, "_blank", "noopener,noreferrer");
1377
+ toast.success("Bundle download started");
1378
+ },
1379
+ size: "sm",
1380
+ variant: "outline",
1381
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Download, { "data-icon": "inline-start" }), "Download bundle"]
1382
+ }) : null,
1383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1384
+ className: "w-full",
1385
+ disabled: release.enabled || busy,
1386
+ onClick: () => setConfirmDelete(true),
1387
+ size: "sm",
1388
+ variant: "destructive",
1389
+ children: "Remove from channel"
1390
+ })
1391
+ ]
1392
+ }),
1393
+ release.bundle_id && bundleQuery.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-40 w-full" }) : null,
1394
+ release.bundle_id && bundleQuery.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
1395
+ variant: "destructive",
1396
+ children: [
1397
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
1398
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Build details are unavailable" }),
1399
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Delivery settings are unaffected." })
1400
+ ]
1401
+ }) : null,
1402
+ !release.bundle_id || !bundleQuery.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleMetadata, {
1403
+ bundle,
1404
+ release
1405
+ }) : null,
1406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", {
1407
+ className: "rounded-lg border bg-muted/10 p-4 text-xs",
1408
+ children: [
1409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", {
1410
+ className: "cursor-pointer font-medium",
1411
+ children: "Advanced diagnostics"
1412
+ }),
1413
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
1414
+ className: "mt-4 grid gap-3 text-muted-foreground sm:grid-cols-2",
1415
+ children: [
1416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1417
+ className: "sm:col-span-2",
1418
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Release ID" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1419
+ className: "mt-1 break-all font-mono text-foreground",
1420
+ children: release.id
1421
+ })]
1422
+ }),
1423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Revision" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1424
+ className: "mt-1 font-mono text-foreground",
1425
+ children: release.revision
1426
+ })] }),
1427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Catalog generation" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1428
+ className: "mt-1 font-mono text-foreground",
1429
+ children: diagnostics.data?.generation ?? "—"
1430
+ })] }),
1431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Catalog size" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1432
+ className: "mt-1 font-mono text-foreground",
1433
+ children: diagnostics.data ? `${diagnostics.data.byte_size} bytes` : "—"
1434
+ })] }),
1435
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1436
+ className: "sm:col-span-2",
1437
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Scope key" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1438
+ className: "mt-1 break-all font-mono text-foreground",
1439
+ children: release.scope_key
1440
+ })]
1441
+ })
1442
+ ]
1443
+ }),
1444
+ diagnostics.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1445
+ className: "mt-3 text-destructive",
1446
+ children: "Diagnostics are unavailable. Bundle settings are unaffected."
1447
+ }) : null
1448
+ ]
1449
+ })
1450
+ ]
1451
+ }) : null]
1452
+ })]
1453
+ })
1454
+ }),
1455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialog$1, {
1456
+ onOpenChange: setConfirmDiscard,
1457
+ open: confirmDiscard,
1458
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogTitle, { children: "Discard unsaved changes?" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogDescription, { children: "Your Bundle changes will be lost." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogCancel, { children: "Keep editing" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogAction, {
1459
+ onClick: () => {
1460
+ setConfirmDiscard(false);
1461
+ onOpenChange(false);
1462
+ },
1463
+ variant: "destructive",
1464
+ children: "Discard changes"
1465
+ })] })] })
1466
+ }),
1467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialog$1, {
1468
+ onOpenChange: setConfirmDelete,
1469
+ open: confirmDelete,
1470
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogTitle, { children: [
1471
+ "Remove this Bundle from ",
1472
+ channelName ?? "the channel",
1473
+ "?"
1474
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogDescription, { children: "The Bundle file stays available. This only removes it from the channel." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogAction, {
1475
+ onClick: async () => {
1476
+ if (!release) return;
1477
+ try {
1478
+ await remove.mutateAsync({
1479
+ expectedRevision: release.revision,
1480
+ releaseId: release.id
1481
+ });
1482
+ toast.success(`Bundle removed from ${channelName ?? "the channel"}`);
1483
+ setConfirmDelete(false);
1484
+ onOpenChange(false);
1485
+ } catch (caught) {
1486
+ setError(caught instanceof Error ? caught.message : "Bundle could not be removed from the channel.");
1487
+ }
1488
+ },
1489
+ variant: "destructive",
1490
+ children: "Remove from channel"
1491
+ })] })] })
1492
+ }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
1494
+ onOpenChange: handlePromoteOpenChange,
1495
+ open: showPromote,
1496
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogContent, {
1497
+ showCloseButton: !promote.isPending,
1498
+ children: [
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle$1, { children: "Promote to Channel" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription$1, { children: "Choose how to promote this Bundle, then select the target channel." })] }),
1500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1501
+ className: "space-y-4 py-4",
1502
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1503
+ className: "space-y-2",
1504
+ children: [
1505
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
1506
+ htmlFor: "promote-action",
1507
+ children: "Action"
1508
+ }),
1509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1510
+ disabled: promote.isPending,
1511
+ items: promoteActionItems,
1512
+ onValueChange: (value) => setPromoteAction(value),
1513
+ value: promoteAction,
1514
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1515
+ className: "w-full",
1516
+ id: "promote-action",
1517
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { placeholder: "Select an action" })
1518
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1519
+ value: "move",
1520
+ children: "Move bundle"
1521
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1522
+ value: "copy",
1523
+ children: "Copy bundle"
1524
+ })] }) })]
1525
+ }),
1526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1527
+ className: "text-xs text-muted-foreground",
1528
+ children: isCopyPromotion ? "Make this Bundle available in the target channel and keep it in the current channel." : "Move this Bundle to the target channel. Devices in the current channel may fall back to an earlier Bundle."
1529
+ })
1530
+ ]
1531
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1532
+ className: "space-y-2",
1533
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
1534
+ htmlFor: "target-channel",
1535
+ children: "Target Channel"
1536
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1537
+ disabled: promote.isPending,
1538
+ items: availableChannelItems,
1539
+ onValueChange: (value) => setTargetChannel(value ?? ""),
1540
+ value: targetChannel || null,
1541
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1542
+ className: "w-full",
1543
+ id: "target-channel",
1544
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { placeholder: "Select a channel" })
1545
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, {
1546
+ className: "max-h-64",
1547
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectGroup, { children: availableChannels.map((channel) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1548
+ value: channel.name,
1549
+ children: channel.name
1550
+ }, channel.id)) })
1551
+ })]
1552
+ })]
1553
+ })]
1554
+ }),
1555
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1556
+ disabled: promote.isPending,
1557
+ onClick: () => handlePromoteOpenChange(false),
1558
+ variant: "outline",
1559
+ children: "Cancel"
1560
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1561
+ disabled: !normalizedTargetChannel || promote.isPending,
1562
+ onClick: async () => {
1563
+ if (!release) return;
1564
+ try {
1565
+ await promote.mutateAsync({
1566
+ action: promoteAction,
1567
+ expectedRevision: release.revision,
1568
+ releaseId: release.id,
1569
+ targetChannel: normalizedTargetChannel
1570
+ });
1571
+ handlePromoteOpenChange(false);
1572
+ toast.success(isCopyPromotion ? `Bundle copied to ${normalizedTargetChannel}` : `Bundle moved to ${normalizedTargetChannel}`, release.bundle_id ? { description: `bundleId: ${release.bundle_id}` } : void 0);
1573
+ } catch (caught) {
1574
+ toast.error(caught instanceof Error ? caught.message : "Bundle could not be promoted.");
1575
+ }
1576
+ },
1577
+ children: promote.isPending ? isCopyPromotion ? "Copying…" : "Moving…" : isCopyPromotion ? "Copy" : "Move"
1578
+ })] })
1579
+ ]
1580
+ })
1581
+ })
1582
+ ] });
1583
+ }
1584
+ function ReleaseStateBadge({ release }) {
1585
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
1586
+ className: "gap-1 font-normal text-muted-foreground",
1587
+ variant: release.enabled ? "outline" : "secondary",
1588
+ children: [release.enabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "size-3" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleOff, { className: "size-3" }), release.enabled ? "Enabled" : "Disabled"]
1589
+ });
1590
+ }
1591
+ function RolloutPercentageBadge({ percentage, className }) {
1592
+ const isPartialRollout = percentage < 100;
1593
+ const formattedPercentage = percentage.toFixed(1);
1594
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
1595
+ className: cn("gap-1", className),
1596
+ variant: isPartialRollout ? "secondary" : "default",
1597
+ children: [
1598
+ isPartialRollout ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, { className: "h-3 w-3" }) : null,
1599
+ formattedPercentage,
1600
+ "%"
1601
+ ]
1602
+ });
1603
+ }
1604
+ var PAGE_SIZE = 20;
1605
+ var platformFilterItems = [
1606
+ {
1607
+ label: "All Platforms",
1608
+ value: "all"
1609
+ },
1610
+ {
1611
+ label: "iOS",
1612
+ value: "ios"
1613
+ },
1614
+ {
1615
+ label: "Android",
1616
+ value: "android"
1617
+ }
1618
+ ];
1619
+ var dateFormatter = new Intl.DateTimeFormat(void 0, {
1620
+ dateStyle: "medium",
1621
+ timeStyle: "short"
1622
+ });
1623
+ var tableDateFormatter = new Intl.DateTimeFormat(void 0, { dateStyle: "medium" });
1624
+ var shortId = (id) => id.length > 18 ? `${id.slice(0, 8)}…${id.slice(-4)}` : id;
1625
+ function BundleMovementSummary({ summary }) {
1626
+ if (summary === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1627
+ "aria-label": "30-day Bundle activity unavailable",
1628
+ className: "text-sm text-muted-foreground",
1629
+ title: "Analytics are unavailable for this Bundle.",
1630
+ children: "—"
1631
+ });
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1633
+ "aria-label": "Bundle movement over 30 days, distinct installations",
1634
+ className: "flex min-w-[140px] items-baseline gap-3 whitespace-nowrap",
1635
+ role: "group",
1636
+ title: "Distinct installations over the last 30 days.",
1637
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1638
+ className: "flex items-baseline gap-1.5",
1639
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1640
+ className: "text-xs text-muted-foreground",
1641
+ children: "Applied"
1642
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1643
+ className: "text-sm font-medium tabular-nums",
1644
+ children: summary.installed
1645
+ })]
1646
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1647
+ className: "flex items-baseline gap-1.5",
1648
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1649
+ className: "text-xs text-muted-foreground",
1650
+ children: "Recovered"
1651
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1652
+ className: "text-sm font-medium tabular-nums",
1653
+ children: summary.recovered
1654
+ })]
1655
+ })]
1656
+ });
1657
+ }
1658
+ function BundleFilterToolbar({ channels, onChange, onClear, onManageChannels, search }) {
1659
+ const [targetAppVersion, setTargetAppVersion] = (0, import_react.useState)(search.targetAppVersion ?? "");
1660
+ const hasFilters = Boolean(search.bundleId || search.channelId || search.enabled !== void 0 || search.platform || search.targetAppVersion);
1661
+ const channelFilterItems = [{
1662
+ label: "All Channels",
1663
+ value: "all"
1664
+ }, ...channels.map((channel) => ({
1665
+ label: channel.name,
1666
+ value: channel.id
1667
+ }))];
1668
+ const applyTargetAppVersion = () => {
1669
+ onChange({ targetAppVersion: targetAppVersion.trim() || void 0 });
1670
+ };
1671
+ (0, import_react.useEffect)(() => {
1672
+ setTargetAppVersion(search.targetAppVersion ?? "");
1673
+ }, [search.targetAppVersion]);
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
1675
+ className: "sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-2 border-b bg-background px-3 py-3 sm:h-12 sm:flex-nowrap sm:bg-card/70 sm:px-4 sm:py-0 sm:backdrop-blur-sm",
1676
+ children: [
1677
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarTrigger, { className: "-ml-1" }),
1678
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
1679
+ className: "sr-only",
1680
+ children: "Bundles"
1681
+ }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1683
+ className: "ml-1 flex items-center gap-1.5 text-muted-foreground sm:ml-2",
1684
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Funnel, { className: "size-3.5" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1685
+ className: "text-xs font-medium",
1686
+ children: "Filters"
1687
+ })]
1688
+ }),
1689
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1690
+ items: platformFilterItems,
1691
+ onValueChange: (value) => onChange({ platform: value === "all" ? void 0 : value }),
1692
+ value: search.platform ?? "all",
1693
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1694
+ "aria-label": "Platform",
1695
+ className: "h-8 w-[calc(50%-0.25rem)] min-w-[132px] text-xs sm:w-[140px]",
1696
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {})
1697
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectGroup, { children: [
1698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1699
+ value: "all",
1700
+ children: "All Platforms"
1701
+ }),
1702
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1703
+ value: "ios",
1704
+ children: "iOS"
1705
+ }),
1706
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1707
+ value: "android",
1708
+ children: "Android"
1709
+ })
1710
+ ] }) })]
1711
+ }),
1712
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1713
+ "aria-label": "Target app version",
1714
+ className: "h-8 w-full min-w-[132px] text-xs sm:w-[160px]",
1715
+ onBlur: applyTargetAppVersion,
1716
+ onChange: (event) => setTargetAppVersion(event.target.value),
1717
+ onKeyDown: (event) => {
1718
+ if (event.key === "Enter") applyTargetAppVersion();
1719
+ },
1720
+ placeholder: "Target version",
1721
+ value: targetAppVersion
1722
+ }),
1723
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1724
+ items: channelFilterItems,
1725
+ onValueChange: (value) => onChange({ channelId: value === null || value === "all" ? void 0 : value }),
1726
+ value: search.channelId ?? "all",
1727
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1728
+ "aria-label": "Channel",
1729
+ className: "h-8 w-[calc(50%-0.25rem)] min-w-[132px] text-xs sm:w-[140px]",
1730
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {})
1731
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1732
+ value: "all",
1733
+ children: "All Channels"
1734
+ }), channels.map((channel) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1735
+ value: channel.id,
1736
+ children: channel.name
1737
+ }, channel.id))] }) })]
1738
+ }),
1739
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1740
+ "aria-label": "Manage channels",
1741
+ onClick: onManageChannels,
1742
+ size: "icon-sm",
1743
+ title: "Manage channels",
1744
+ variant: "outline",
1745
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tags, {})
1746
+ }),
1747
+ search.bundleId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
1748
+ className: "max-w-48 gap-1",
1749
+ variant: "secondary",
1750
+ children: [
1751
+ "Bundle ",
1752
+ shortId(search.bundleId),
1753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
1754
+ "aria-label": "Clear Bundle filter",
1755
+ className: "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1756
+ onClick: () => onChange({ bundleId: void 0 }),
1757
+ type: "button",
1758
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { className: "size-3" })
1759
+ })
1760
+ ]
1761
+ }) : null,
1762
+ hasFilters ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1763
+ className: "h-8 px-2 text-xs text-muted-foreground hover:text-foreground sm:ml-auto",
1764
+ onClick: onClear,
1765
+ size: "sm",
1766
+ variant: "ghost",
1767
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { "data-icon": "inline-start" }), "Clear"]
1768
+ }) : null
1769
+ ]
1770
+ });
1771
+ }
1772
+ function BundleEntry({ expanded, onOpen, onToggleExpand, release }) {
1773
+ const bundleLabel = release.bundle_id ? `bundle ${release.bundle_id}` : "the built-in app deployment";
1774
+ const operationLabel = release.operation === "PROMOTE" ? "Promoted" : release.operation === "ROLLBACK" ? "Rollback" : null;
1775
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1776
+ className: "flex min-w-[240px] items-center gap-3",
1777
+ children: [
1778
+ release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1779
+ "aria-controls": `bundle-lineage-panel-${release.id}`,
1780
+ "aria-expanded": expanded,
1781
+ "aria-label": expanded ? "Hide Lineage" : "Show Lineage",
1782
+ className: "size-8 shrink-0 touch-manipulation",
1783
+ onClick: onToggleExpand,
1784
+ size: "icon",
1785
+ type: "button",
1786
+ variant: "ghost",
1787
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, {})
1788
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1789
+ "aria-hidden": "true",
1790
+ className: "size-8 shrink-0"
1791
+ }),
1792
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
1793
+ "aria-label": `Open details for ${bundleLabel}`,
1794
+ className: "min-w-0 rounded-sm text-left text-foreground transition-colors underline-offset-4 hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1795
+ onClick: onOpen,
1796
+ title: release.bundle_id ?? release.id,
1797
+ type: "button",
1798
+ children: release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleIdDisplay, {
1799
+ bundleId: release.bundle_id,
1800
+ fullOnMobile: true
1801
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1802
+ className: "inline-flex items-center gap-1.5 text-sm",
1803
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RotateCcw, { className: "size-3.5" }), " Built-in app"]
1804
+ })
1805
+ }),
1806
+ operationLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
1807
+ className: "shrink-0 font-normal",
1808
+ variant: "secondary",
1809
+ children: operationLabel
1810
+ }) : null,
1811
+ release.currentlyUnreachable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1812
+ "aria-label": "Currently unreachable. No catalog segment or cohort selects this release first.",
1813
+ className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground",
1814
+ title: "No catalog segment or cohort selects this release first with the current delivery settings.",
1815
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleOff, { className: "size-3.5" }), "Unreachable"]
1816
+ }) : null
1817
+ ]
1818
+ });
1819
+ }
1820
+ function ReleaseBundleChildrenPanel({ onDetailClick, panelId, release }) {
1821
+ const bundleId = release.bundle_id ?? "";
1822
+ const bundleQuery = useBundleQuery(bundleId);
1823
+ const childrenQuery = useBundleChildrenQuery(bundleId);
1824
+ if (bundleQuery.isError || childrenQuery.isError) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1825
+ "aria-live": "polite",
1826
+ className: "border-t bg-muted/10 p-4 text-sm text-muted-foreground",
1827
+ id: panelId,
1828
+ children: "Patch lineage could not be loaded."
1829
+ });
1830
+ if (bundleQuery.isPending) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1831
+ "aria-live": "polite",
1832
+ className: "flex flex-col gap-2 border-t bg-muted/10 p-4",
1833
+ id: panelId,
1834
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" })]
1835
+ });
1836
+ if (!bundleQuery.data) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1837
+ "aria-live": "polite",
1838
+ className: "border-t bg-muted/10 p-4 text-sm text-muted-foreground",
1839
+ id: panelId,
1840
+ children: "Bundle details are unavailable."
1841
+ });
1842
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleChildrenPanel, {
1843
+ bundle: bundleQuery.data,
1844
+ bundles: childrenQuery.data ?? [],
1845
+ loading: childrenQuery.isPending,
1846
+ onDetailClick,
1847
+ panelId
1848
+ });
1849
+ }
1850
+ function BundlesPage() {
1851
+ const search = Route.useSearch();
1852
+ const navigate = Route.useNavigate();
1853
+ const isMobile = useIsMobile();
1854
+ const [channelsOpen, setChannelsOpen] = (0, import_react.useState)(false);
1855
+ const [expandedReleaseId, setExpandedReleaseId] = (0, import_react.useState)();
1856
+ const releasesQuery = useReleasesQuery({
1857
+ afterReleaseId: search.afterReleaseId,
1858
+ beforeReleaseId: search.beforeReleaseId,
1859
+ bundleId: search.bundleId,
1860
+ channelId: search.channelId,
1861
+ enabled: search.enabled,
1862
+ limit: PAGE_SIZE,
1863
+ page: search.page,
1864
+ platform: search.platform,
1865
+ targetAppVersion: search.targetAppVersion
1866
+ });
1867
+ const channels = useChannelsQuery().data ?? [];
1868
+ const releases = releasesQuery.data?.data ?? [];
1869
+ const patchCountsByBundleId = useBundleChildCountsQuery(Array.from(new Set(releases.flatMap((release) => release.bundle_id ? [release.bundle_id] : [])))).data ?? {};
1870
+ const pagination = releasesQuery.data?.pagination;
1871
+ const channelNames = new Map(channels.map((channel) => [channel.id, channel.name]));
1872
+ (0, import_react.useEffect)(() => {
1873
+ if (expandedReleaseId && !releases.some((release) => release.id === expandedReleaseId)) setExpandedReleaseId(void 0);
1874
+ }, [expandedReleaseId, releases]);
1875
+ const go = (nextSearch, resetScroll = true) => void navigate({
1876
+ resetScroll,
1877
+ search: nextSearch,
1878
+ to: "/"
1879
+ });
1880
+ const changeFilters = (filters) => go(updateReleaseFilters(search, filters));
1881
+ const openChildBundle = (bundle) => {
1882
+ const childRelease = releases.find((release) => release.bundle_id === bundle.id);
1883
+ setExpandedReleaseId(void 0);
1884
+ if (childRelease) {
1885
+ go({
1886
+ ...search,
1887
+ releaseId: childRelease.id
1888
+ }, false);
1889
+ return;
1890
+ }
1891
+ changeFilters({ bundleId: bundle.id });
1892
+ };
1893
+ const currentPage = pagination?.currentPage ?? 1;
1894
+ const startEntry = releases.length === 0 ? 0 : (currentPage - 1) * PAGE_SIZE + 1;
1895
+ const endEntry = startEntry === 0 ? 0 : startEntry + releases.length - 1;
1896
+ const previousPage = currentPage - 1;
1897
+ const firstReleaseId = releases[0]?.id;
1898
+ const lastReleaseId = releases.at(-1)?.id;
1899
+ const previousSearch = pagination?.hasPreviousPage && firstReleaseId ? {
1900
+ ...search,
1901
+ afterReleaseId: firstReleaseId,
1902
+ beforeReleaseId: void 0,
1903
+ page: previousPage > 1 ? previousPage : void 0,
1904
+ releaseId: void 0
1905
+ } : null;
1906
+ const nextSearch = pagination?.hasNextPage && lastReleaseId ? {
1907
+ ...search,
1908
+ afterReleaseId: void 0,
1909
+ beforeReleaseId: lastReleaseId,
1910
+ page: currentPage + 1,
1911
+ releaseId: void 0
1912
+ } : null;
1913
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1914
+ className: "flex h-svh min-h-0 min-w-0 flex-col",
1915
+ children: [
1916
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleFilterToolbar, {
1917
+ channels,
1918
+ onChange: changeFilters,
1919
+ onClear: () => go({}),
1920
+ onManageChannels: () => setChannelsOpen(true),
1921
+ search
1922
+ }),
1923
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1924
+ className: "flex min-h-0 min-w-0 flex-1 flex-col gap-6 bg-muted/5 p-3 sm:p-6",
1925
+ children: [releasesQuery.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
1926
+ variant: "destructive",
1927
+ children: [
1928
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
1929
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Bundles could not be loaded" }),
1930
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: releasesQuery.error.message })
1931
+ ]
1932
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1933
+ className: "min-h-0 min-w-0 flex-1 overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm [&_[data-slot=table-container]]:h-full [&_[data-slot=table-container]]:overflow-auto",
1934
+ children: isMobile ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1935
+ className: "h-full overflow-y-auto",
1936
+ children: releasesQuery.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1937
+ className: "flex flex-col gap-3 p-4",
1938
+ children: Array.from({ length: 5 }, (_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-44 w-full" }, index))
1939
+ }) : releases.length ? releases.map((release) => {
1940
+ const channelName = channelNames.get(release.channel_id) ?? release.channel_id;
1941
+ const patchCount = release.bundle_id ? patchCountsByBundleId[release.bundle_id] : 0;
1942
+ const isExpanded = expandedReleaseId === release.id;
1943
+ const panelId = `bundle-lineage-panel-${release.id}`;
1944
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", {
1945
+ className: cn("flex flex-col gap-4 border-b p-4 last:border-b-0", release.currentlyUnreachable && "bg-muted/35 saturate-50 transition-[background-color,filter] focus-within:saturate-100 motion-reduce:transition-none", isExpanded && "border-b-0 bg-primary/5"),
1946
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1947
+ className: "flex items-start justify-between gap-3",
1948
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleEntry, {
1949
+ expanded: isExpanded,
1950
+ onOpen: () => go({
1951
+ ...search,
1952
+ releaseId: release.id
1953
+ }, false),
1954
+ onToggleExpand: () => setExpandedReleaseId(isExpanded ? void 0 : release.id),
1955
+ release
1956
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannelBadge, {
1957
+ channel: channelName,
1958
+ className: "shrink-0"
1959
+ })]
1960
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
1961
+ className: "grid grid-cols-2 gap-3 text-xs",
1962
+ children: [
1963
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1964
+ className: "rounded-md bg-muted/40 p-3",
1965
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1966
+ className: "text-muted-foreground",
1967
+ children: "Platform"
1968
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dd", {
1969
+ className: "mt-1 flex items-center gap-2 font-medium",
1970
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
1971
+ className: "size-4",
1972
+ platform: release.platform
1973
+ }), release.platform === "ios" ? "iOS" : "Android"]
1974
+ })]
1975
+ }),
1976
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1977
+ className: "rounded-md bg-muted/40 p-3",
1978
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1979
+ className: "text-muted-foreground",
1980
+ children: "Created"
1981
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1982
+ className: "mt-1 tabular-nums",
1983
+ children: dateFormatter.format(release.created_at_ms)
1984
+ })]
1985
+ }),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1987
+ className: "rounded-md bg-muted/40 p-3",
1988
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1989
+ className: "text-muted-foreground",
1990
+ children: "Rollout"
1991
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1992
+ className: "mt-1",
1993
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RolloutPercentageBadge, { percentage: release.rollout_cohort_count / 10 })
1994
+ })]
1995
+ }),
1996
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1997
+ className: "rounded-md bg-muted/40 p-3",
1998
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1999
+ className: "text-muted-foreground",
2000
+ children: "Patches"
2001
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
2002
+ className: "mt-1",
2003
+ children: patchCount === void 0 ? "Checking" : patchCount > 0 ? `${patchCount} ${patchCount === 1 ? "patch" : "patches"}` : "—"
2004
+ })]
2005
+ }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2007
+ className: "col-span-2 rounded-md bg-muted/40 p-3",
2008
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
2009
+ className: "text-muted-foreground",
2010
+ children: "Activity · 30d"
2011
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
2012
+ className: "mt-1",
2013
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleMovementSummary, { summary: release.activity30d })
2014
+ })]
2015
+ }),
2016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2017
+ className: "col-span-2 flex flex-wrap items-center gap-2 rounded-md bg-muted/40 p-3",
2018
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReleaseStateBadge, { release }), release.should_force_update ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
2019
+ variant: "secondary",
2020
+ children: "Force update"
2021
+ }) : null]
2022
+ }),
2023
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2024
+ className: "col-span-2 grid gap-2 rounded-md border bg-background/80 p-3",
2025
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2026
+ className: "flex items-start justify-between gap-3",
2027
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
2028
+ className: "text-muted-foreground",
2029
+ children: "Target"
2030
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
2031
+ className: "min-w-0 truncate text-right font-mono",
2032
+ children: release.target_app_version ?? release.fingerprint_hash ?? "—"
2033
+ })]
2034
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2035
+ className: "flex items-start justify-between gap-3",
2036
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
2037
+ className: "text-muted-foreground",
2038
+ children: "Message"
2039
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
2040
+ className: "min-w-0 text-right text-foreground",
2041
+ children: release.message || "—"
2042
+ })]
2043
+ })]
2044
+ })
2045
+ ]
2046
+ })]
2047
+ }), isExpanded && release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReleaseBundleChildrenPanel, {
2048
+ onDetailClick: openChildBundle,
2049
+ panelId,
2050
+ release
2051
+ }) : null] }, release.id);
2052
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
2053
+ className: "p-8 text-center text-sm text-muted-foreground",
2054
+ children: search.bundleId || search.channelId || search.enabled !== void 0 || search.platform || search.targetAppVersion ? "No bundles match these filters." : "No bundles yet. Deploy a bundle to see it here."
2055
+ })
2056
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Table, {
2057
+ className: "min-w-max",
2058
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHeader, {
2059
+ className: "bg-muted/40",
2060
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, {
2061
+ className: "border-b border-border/60 hover:bg-transparent [&>th]:h-10 [&>th]:text-xs [&>th]:font-semibold [&>th]:uppercase [&>th]:text-muted-foreground/70",
2062
+ children: [
2063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Bundle ID" }),
2064
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Channel" }),
2065
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Platform" }),
2066
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Patches" }),
2067
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Target" }),
2068
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Enabled" }),
2069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Force update" }),
2070
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Rollout" }),
2071
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, {
2072
+ title: "Distinct installations over the last 30 days.",
2073
+ children: "Activity · 30d"
2074
+ }),
2075
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Message" }),
2076
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Created" })
2077
+ ]
2078
+ })
2079
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableBody, { children: [releasesQuery.isPending ? Array.from({ length: 7 }, (_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2080
+ colSpan: 11,
2081
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-8 w-full" })
2082
+ }) }, index)) : releases.map((release) => {
2083
+ const isExpanded = expandedReleaseId === release.id;
2084
+ const panelId = `bundle-lineage-panel-${release.id}`;
2085
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, {
2086
+ "aria-label": `Open bundle ${release.bundle_id ?? release.id}`,
2087
+ className: cn("cursor-pointer transition-[background-color,filter] hover:bg-muted/10 focus-within:bg-muted/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none data-[state=selected]:bg-muted/15 [&>td]:py-3", release.currentlyUnreachable && "bg-muted/35 saturate-50 hover:bg-muted/50 hover:saturate-100 focus-within:saturate-100", isExpanded && "bg-primary/5"),
2088
+ "data-state": search.releaseId === release.id ? "selected" : void 0,
2089
+ onClick: (event) => {
2090
+ if (event.target instanceof Element && event.target.closest("button, a, input, select, textarea")) return;
2091
+ go({
2092
+ ...search,
2093
+ releaseId: release.id
2094
+ }, false);
2095
+ },
2096
+ onKeyDown: (event) => {
2097
+ if (event.target !== event.currentTarget || event.key !== "Enter" && event.key !== " ") return;
2098
+ event.preventDefault();
2099
+ go({
2100
+ ...search,
2101
+ releaseId: release.id
2102
+ }, false);
2103
+ },
2104
+ tabIndex: 0,
2105
+ children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleEntry, {
2107
+ expanded: isExpanded,
2108
+ onOpen: () => go({
2109
+ ...search,
2110
+ releaseId: release.id
2111
+ }, false),
2112
+ onToggleExpand: () => setExpandedReleaseId(isExpanded ? void 0 : release.id),
2113
+ release
2114
+ }) }),
2115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannelBadge, { channel: channelNames.get(release.channel_id) ?? release.channel_id }) }),
2116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2117
+ className: "whitespace-nowrap",
2118
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2119
+ className: "flex items-center gap-2",
2120
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
2121
+ className: "size-4",
2122
+ platform: release.platform
2123
+ }), release.platform === "ios" ? "iOS" : "Android"]
2124
+ })
2125
+ }),
2126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: release.bundle_id && patchCountsByBundleId[release.bundle_id] === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2127
+ className: "text-sm text-muted-foreground",
2128
+ children: "Checking"
2129
+ }) : release.bundle_id && patchCountsByBundleId[release.bundle_id] > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
2130
+ variant: "secondary",
2131
+ children: [
2132
+ patchCountsByBundleId[release.bundle_id],
2133
+ " ",
2134
+ patchCountsByBundleId[release.bundle_id] === 1 ? "patch" : "patches"
2135
+ ]
2136
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2137
+ className: "text-muted-foreground",
2138
+ children: "-"
2139
+ }) }),
2140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: release.target_app_version ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2141
+ className: "flex items-center gap-2",
2142
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Package, { className: "size-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2143
+ className: "font-medium",
2144
+ children: release.target_app_version
2145
+ })]
2146
+ }) : release.fingerprint_hash ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2147
+ className: "flex min-w-[160px] items-center gap-2",
2148
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FingerprintPattern, { className: "size-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HashValueDisplay, {
2149
+ maxLength: 12,
2150
+ value: release.fingerprint_hash
2151
+ })]
2152
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2153
+ className: "text-muted-foreground",
2154
+ children: "-"
2155
+ }) }),
2156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2157
+ className: "flex items-center",
2158
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnabledStatusIcon, { enabled: release.enabled }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2159
+ className: "sr-only",
2160
+ children: release.enabled ? "Enabled" : "Disabled"
2161
+ })]
2162
+ }) }),
2163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2164
+ className: "flex items-center",
2165
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnabledStatusIcon, {
2166
+ enabled: release.should_force_update,
2167
+ falseIcon: "minus"
2168
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2169
+ className: "sr-only",
2170
+ children: release.should_force_update ? "Required" : "Optional"
2171
+ })]
2172
+ }) }),
2173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RolloutPercentageBadge, { percentage: release.rollout_cohort_count / 10 }) }),
2174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BundleMovementSummary, { summary: release.activity30d }) }),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2176
+ className: "text-sm text-muted-foreground",
2177
+ title: release.message ?? void 0,
2178
+ children: release.message || "-"
2179
+ }),
2180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2181
+ className: "whitespace-nowrap text-xs text-muted-foreground",
2182
+ title: dateFormatter.format(release.created_at_ms),
2183
+ children: tableDateFormatter.format(release.created_at_ms)
2184
+ })
2185
+ ]
2186
+ }), isExpanded && release.bundle_id ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableRow, {
2187
+ className: "hover:bg-transparent",
2188
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2189
+ className: "border-t-0 p-0",
2190
+ colSpan: 11,
2191
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReleaseBundleChildrenPanel, {
2192
+ onDetailClick: openChildBundle,
2193
+ panelId,
2194
+ release
2195
+ })
2196
+ })
2197
+ }) : null] }, release.id);
2198
+ }), !releasesQuery.isPending && releases.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
2199
+ className: "h-40 text-center text-muted-foreground",
2200
+ colSpan: 10,
2201
+ children: search.bundleId || search.channelId || search.enabled !== void 0 || search.platform || search.targetAppVersion ? "No bundles match these filters." : "No bundles yet. Deploy a bundle to see it here."
2202
+ }) }) : null] })]
2203
+ })
2204
+ }), !releasesQuery.isError && !releasesQuery.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("nav", {
2205
+ "aria-label": "Bundle pagination",
2206
+ className: "flex flex-col gap-3 px-2 sm:flex-row sm:items-center sm:justify-between",
2207
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2208
+ className: "text-xs font-medium text-muted-foreground",
2209
+ children: [
2210
+ "Showing ",
2211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2212
+ className: "text-foreground",
2213
+ children: startEntry
2214
+ }),
2215
+ " to",
2216
+ " ",
2217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2218
+ className: "text-foreground",
2219
+ children: endEntry
2220
+ }),
2221
+ " entries"
2222
+ ]
2223
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2224
+ className: "flex flex-wrap items-center gap-3 sm:justify-end",
2225
+ children: [
2226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2227
+ className: "text-xs font-medium text-muted-foreground",
2228
+ children: ["Page ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2229
+ className: "text-foreground",
2230
+ children: currentPage
2231
+ })]
2232
+ }),
2233
+ previousSearch ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Link, {
2234
+ className: buttonVariants({
2235
+ className: "h-8 flex-1 px-3 text-xs sm:flex-none",
2236
+ size: "sm",
2237
+ variant: "outline"
2238
+ }),
2239
+ search: previousSearch,
2240
+ to: "/",
2241
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronLeft, { "data-icon": "inline-start" }), "Previous"]
2242
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2243
+ className: "h-8 flex-1 px-3 text-xs sm:flex-none",
2244
+ disabled: true,
2245
+ size: "sm",
2246
+ variant: "outline",
2247
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronLeft, { "data-icon": "inline-start" }), "Previous"]
2248
+ }),
2249
+ nextSearch ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Link, {
2250
+ className: buttonVariants({
2251
+ className: "h-8 flex-1 px-3 text-xs sm:flex-none",
2252
+ size: "sm",
2253
+ variant: "outline"
2254
+ }),
2255
+ search: nextSearch,
2256
+ to: "/",
2257
+ children: ["Next", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { "data-icon": "inline-end" })]
2258
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2259
+ className: "h-8 flex-1 px-3 text-xs sm:flex-none",
2260
+ disabled: true,
2261
+ size: "sm",
2262
+ variant: "outline",
2263
+ children: ["Next", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { "data-icon": "inline-end" })]
2264
+ })
2265
+ ]
2266
+ })]
2267
+ }) : null]
2268
+ }),
2269
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReleaseEditorSheet, {
2270
+ channels,
2271
+ onOpenChange: (open) => !open && go({
2272
+ ...search,
2273
+ releaseId: void 0
2274
+ }, false),
2275
+ open: Boolean(search.releaseId),
2276
+ releaseId: search.releaseId ?? ""
2277
+ }),
2278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannelManagementDialog, {
2279
+ onOpenChange: setChannelsOpen,
2280
+ open: channelsOpen
2281
+ })
2282
+ ]
2283
+ });
2284
+ }
2285
+ //#endregion
2286
+ export { BundlesPage as component };