@wopr-network/platform-ui-core 1.27.8 → 1.27.9

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 (353) hide show
  1. package/next.config.ts +1 -2
  2. package/package.json +17 -17
  3. package/src/__tests__/account-switcher.test.tsx +21 -20
  4. package/src/__tests__/activity-page.test.tsx +2 -6
  5. package/src/__tests__/add-payment-method-dialog.test.tsx +9 -32
  6. package/src/__tests__/admin-api.test.ts +1 -6
  7. package/src/__tests__/admin-gpu-api.test.ts +1 -3
  8. package/src/__tests__/admin-marketplace-api.test.ts +1 -4
  9. package/src/__tests__/admin-middleware.test.ts +76 -83
  10. package/src/__tests__/affiliate-dashboard.test.tsx +3 -3
  11. package/src/__tests__/api-401-redirect.test.ts +46 -9
  12. package/src/__tests__/api-client.test.ts +3 -5
  13. package/src/__tests__/api-config.test.ts +22 -42
  14. package/src/__tests__/api-fleet-resources.test.ts +1 -2
  15. package/src/__tests__/api-fleet-trpc.test.ts +2 -8
  16. package/src/__tests__/api-null-guards.test.ts +3 -1
  17. package/src/__tests__/audit-log-table-pagination.test.tsx +2 -6
  18. package/src/__tests__/auth-password-reset.test.tsx +7 -21
  19. package/src/__tests__/auth-redirect.test.tsx +8 -2
  20. package/src/__tests__/auth.test.tsx +25 -23
  21. package/src/__tests__/auto-topup-card.test.tsx +4 -12
  22. package/src/__tests__/backups-tab.test.tsx +3 -4
  23. package/src/__tests__/billing-layout-nav-hidden.test.tsx +5 -37
  24. package/src/__tests__/billing-payment-org-invoices.test.tsx +2 -18
  25. package/src/__tests__/billing.test.tsx +8 -39
  26. package/src/__tests__/bot-settings/resources-tab.test.tsx +1 -3
  27. package/src/__tests__/bot-settings/storage-tab.test.tsx +1 -3
  28. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +1 -3
  29. package/src/__tests__/bot-settings-restart.test.tsx +1 -3
  30. package/src/__tests__/bot-settings.test.tsx +2 -6
  31. package/src/__tests__/brand.test.ts +6 -26
  32. package/src/__tests__/buy-credits-panel.test.tsx +1 -3
  33. package/src/__tests__/buy-crypto-credits-panel.test.tsx +101 -119
  34. package/src/__tests__/capability-conflicts.test.ts +2 -8
  35. package/src/__tests__/capability-resolver.test.tsx +2 -12
  36. package/src/__tests__/channel-wizard.test.tsx +4 -17
  37. package/src/__tests__/chat/chat-panel.test.tsx +1 -4
  38. package/src/__tests__/chat-store.test.ts +5 -15
  39. package/src/__tests__/command-center.test.tsx +10 -12
  40. package/src/__tests__/compliance-retention-edit.test.tsx +3 -6
  41. package/src/__tests__/confirmation-tracker.test.tsx +3 -18
  42. package/src/__tests__/coupon-input.test.tsx +1 -3
  43. package/src/__tests__/create-instance.test.tsx +1 -3
  44. package/src/__tests__/credit-balance.test.tsx +4 -12
  45. package/src/__tests__/credits.test.tsx +32 -85
  46. package/src/__tests__/email-verification-banner.test.tsx +2 -6
  47. package/src/__tests__/error-boundaries.test.tsx +0 -1
  48. package/src/__tests__/fetch-pricing.test.ts +2 -1
  49. package/src/__tests__/field-oauth.test.tsx +2 -6
  50. package/src/__tests__/fixtures/mock-manifests-data.js +1 -3
  51. package/src/__tests__/fixtures/mock-manifests.ts +2 -4
  52. package/src/__tests__/fleet-health-timestamp.test.tsx +1 -8
  53. package/src/__tests__/fleet-health-update.test.tsx +1 -8
  54. package/src/__tests__/gpu-dashboard.test.tsx +2 -6
  55. package/src/__tests__/instance-detail.test.tsx +3 -9
  56. package/src/__tests__/instance-list.test.tsx +1 -5
  57. package/src/__tests__/layout-snapshots.test.tsx +64 -11
  58. package/src/__tests__/marketplace-admin.test.tsx +2 -6
  59. package/src/__tests__/marketplace.test.tsx +11 -35
  60. package/src/__tests__/merge-api-rates.test.ts +1 -6
  61. package/src/__tests__/middleware.test.ts +32 -219
  62. package/src/__tests__/next-config-headers.test.ts +1 -3
  63. package/src/__tests__/notifications.test.tsx +4 -11
  64. package/src/__tests__/oauth-buttons.test.tsx +36 -59
  65. package/src/__tests__/oauth-error-mapping.test.tsx +2 -6
  66. package/src/__tests__/observability.test.tsx +23 -36
  67. package/src/__tests__/onboarding-page.test.tsx +4 -6
  68. package/src/__tests__/org-billing-api.test.tsx +1 -6
  69. package/src/__tests__/plugin-install-flow.test.tsx +28 -58
  70. package/src/__tests__/plugin-registry.test.tsx +3 -11
  71. package/src/__tests__/plugin-tool-sync.test.ts +1 -3
  72. package/src/__tests__/plugins-catalog-error.test.tsx +2 -6
  73. package/src/__tests__/plugins-toggle-race.test.tsx +3 -5
  74. package/src/__tests__/portfolio-chart.test.tsx +2 -6
  75. package/src/__tests__/promotion-form.test.tsx +2 -6
  76. package/src/__tests__/promotions-list.test.tsx +1 -3
  77. package/src/__tests__/provider-key-api.test.ts +2 -1
  78. package/src/__tests__/resend-verification-button.test.tsx +8 -24
  79. package/src/__tests__/secrets-audit-pagination.test.tsx +1 -3
  80. package/src/__tests__/settings.test.tsx +11 -21
  81. package/src/__tests__/setup-checklist.test.tsx +3 -9
  82. package/src/__tests__/setup.ts +25 -6
  83. package/src/__tests__/snapshot-api.test.ts +2 -1
  84. package/src/__tests__/step-superpowers.test.tsx +1 -3
  85. package/src/__tests__/tenant-context.test.tsx +1 -6
  86. package/src/__tests__/tenant-keys-api.test.ts +3 -4
  87. package/src/__tests__/tenant-table-pagination.test.tsx +2 -6
  88. package/src/__tests__/terminal-log-cleanup.test.tsx +0 -1
  89. package/src/__tests__/transaction-history.test.tsx +190 -238
  90. package/src/__tests__/trpc-types.test.ts +2 -6
  91. package/src/__tests__/use-chat.test.ts +1 -3
  92. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +1 -4
  93. package/src/__tests__/use-sidecar-bridge.test.tsx +105 -0
  94. package/src/__tests__/use-webmcp.test.ts +1 -3
  95. package/src/__tests__/validate-elevenlabs-key.test.ts +2 -1
  96. package/src/__tests__/verify-page.test.tsx +4 -13
  97. package/src/__tests__/verify-redirect.test.tsx +2 -6
  98. package/src/app/(auth)/error.tsx +1 -7
  99. package/src/app/(auth)/forgot-password/page.tsx +4 -18
  100. package/src/app/(auth)/login/page.tsx +5 -22
  101. package/src/app/(auth)/reset-password/page.tsx +2 -12
  102. package/src/app/(auth)/signup/page.tsx +10 -44
  103. package/src/app/(auth)/verify/page.tsx +47 -0
  104. package/src/app/(dashboard)/billing/credits/page.tsx +14 -67
  105. package/src/app/(dashboard)/billing/error.tsx +2 -10
  106. package/src/app/(dashboard)/billing/layout.tsx +12 -62
  107. package/src/app/(dashboard)/billing/payment/page.tsx +17 -68
  108. package/src/app/(dashboard)/billing/plans/page.tsx +3 -9
  109. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +8 -25
  110. package/src/app/(dashboard)/billing/usage/page.tsx +63 -103
  111. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +9 -27
  112. package/src/app/(dashboard)/changesets/[id]/error.tsx +2 -6
  113. package/src/app/(dashboard)/changesets/error.tsx +1 -7
  114. package/src/app/(dashboard)/chat/page.tsx +2 -6
  115. package/src/app/(dashboard)/dashboard/network/page.tsx +5 -19
  116. package/src/app/(dashboard)/error.tsx +1 -7
  117. package/src/app/(dashboard)/layout.tsx +15 -36
  118. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +14 -51
  119. package/src/app/(dashboard)/marketplace/error.tsx +1 -7
  120. package/src/app/(dashboard)/marketplace/page.tsx +6 -27
  121. package/src/app/(dashboard)/not-found.tsx +2 -5
  122. package/src/app/(dashboard)/onboarding/page.tsx +5 -22
  123. package/src/app/(dashboard)/settings/account/page.tsx +1 -6
  124. package/src/app/(dashboard)/settings/activity/page.tsx +8 -34
  125. package/src/app/(dashboard)/settings/api-keys/page.tsx +15 -60
  126. package/src/app/(dashboard)/settings/brain/page.tsx +9 -31
  127. package/src/app/(dashboard)/settings/error.tsx +2 -10
  128. package/src/app/(dashboard)/settings/notifications/page.tsx +2 -6
  129. package/src/app/(dashboard)/settings/org/page.tsx +13 -56
  130. package/src/app/(dashboard)/settings/page.tsx +1 -0
  131. package/src/app/(dashboard)/settings/profile/page.tsx +126 -73
  132. package/src/app/(dashboard)/settings/providers/page.tsx +21 -78
  133. package/src/app/(dashboard)/settings/secrets/page.tsx +13 -58
  134. package/src/app/(dashboard)/settings/security/page.tsx +31 -111
  135. package/src/app/admin/email-templates/email-templates-client.tsx +15 -58
  136. package/src/app/admin/error.tsx +1 -7
  137. package/src/app/admin/fleet-updates/error.tsx +1 -7
  138. package/src/app/admin/fleet-updates/fleet-updates-client.tsx +10 -50
  139. package/src/app/admin/layout.tsx +4 -0
  140. package/src/app/admin/payment-methods/page.tsx +9 -38
  141. package/src/app/admin/products/error.tsx +2 -7
  142. package/src/app/admin/products/page.tsx +1 -4
  143. package/src/app/admin/promotions/[id]/page.tsx +9 -38
  144. package/src/app/admin/promotions/page.tsx +9 -36
  145. package/src/app/admin/rate-overrides/page.tsx +9 -45
  146. package/src/app/auth/callback/[provider]/page.tsx +1 -8
  147. package/src/app/auth/verify/page.tsx +9 -36
  148. package/src/app/channels/error.tsx +2 -10
  149. package/src/app/channels/layout.tsx +9 -0
  150. package/src/app/channels/page.tsx +8 -20
  151. package/src/app/channels/setup/[plugin]/page.tsx +3 -5
  152. package/src/app/error.tsx +1 -7
  153. package/src/app/fleet/error.tsx +1 -7
  154. package/src/app/fleet/layout.tsx +5 -0
  155. package/src/app/fleet/settings/page.tsx +1 -3
  156. package/src/app/global-error.tsx +2 -10
  157. package/src/app/globals.css +1 -4
  158. package/src/app/instances/[id]/instance-detail-client.tsx +51 -125
  159. package/src/app/instances/error.tsx +2 -10
  160. package/src/app/instances/instance-list-client.tsx +20 -69
  161. package/src/app/instances/layout.tsx +9 -0
  162. package/src/app/instances/new/create-instance-client.tsx +10 -31
  163. package/src/app/layout.tsx +2 -10
  164. package/src/app/not-found.tsx +1 -3
  165. package/src/app/page.tsx +1 -2
  166. package/src/app/plugins/error.tsx +2 -10
  167. package/src/app/plugins/layout.tsx +5 -0
  168. package/src/app/plugins/page.tsx +16 -48
  169. package/src/app/pricing/error.tsx +1 -7
  170. package/src/app/privacy/page.tsx +93 -150
  171. package/src/app/status/error.tsx +1 -7
  172. package/src/app/terms/page.tsx +89 -144
  173. package/src/components/account-switcher.tsx +25 -52
  174. package/src/components/admin/accounting-dashboard.tsx +1 -3
  175. package/src/components/admin/admin-guard.tsx +1 -3
  176. package/src/components/admin/admin-nav.tsx +1 -3
  177. package/src/components/admin/affiliate-dashboard.tsx +25 -94
  178. package/src/components/admin/audit-log-table.tsx +13 -49
  179. package/src/components/admin/billing-health-dashboard.tsx +7 -25
  180. package/src/components/admin/bulk-actions-bar.test.tsx +1 -7
  181. package/src/components/admin/bulk-actions-bar.tsx +1 -3
  182. package/src/components/admin/bulk-export-dialog.test.tsx +1 -7
  183. package/src/components/admin/bulk-export-dialog.tsx +6 -32
  184. package/src/components/admin/bulk-grant-dialog.test.tsx +2 -6
  185. package/src/components/admin/bulk-grant-dialog.tsx +4 -15
  186. package/src/components/admin/bulk-preview-dialog.tsx +3 -12
  187. package/src/components/admin/bulk-reactivate-dialog.tsx +1 -7
  188. package/src/components/admin/bulk-select-all-banner.tsx +1 -6
  189. package/src/components/admin/bulk-suspend-dialog.tsx +5 -12
  190. package/src/components/admin/bulk-undo-toast.tsx +1 -2
  191. package/src/components/admin/compliance-dashboard.tsx +31 -101
  192. package/src/components/admin/gpu-dashboard.tsx +21 -70
  193. package/src/components/admin/grant-credits-dialog.tsx +4 -17
  194. package/src/components/admin/incident-dashboard.tsx +10 -25
  195. package/src/components/admin/inference-dashboard.tsx +14 -54
  196. package/src/components/admin/marketplace-admin.tsx +18 -60
  197. package/src/components/admin/migrations-dashboard.tsx +9 -42
  198. package/src/components/admin/onboarding-dashboard.tsx +14 -64
  199. package/src/components/admin/pool-config-dashboard.tsx +4 -10
  200. package/src/components/admin/products/fleet-form.tsx +2 -11
  201. package/src/components/admin/products/nav-editor.tsx +3 -10
  202. package/src/components/admin/promotions/promotion-form.tsx +9 -42
  203. package/src/components/admin/roles-dashboard.tsx +7 -34
  204. package/src/components/admin/suspend-dialog.tsx +4 -11
  205. package/src/components/admin/tenant-notes-panel.tsx +1 -3
  206. package/src/components/admin/tenant-row-actions.tsx +4 -20
  207. package/src/components/admin/tenant-table.tsx +12 -49
  208. package/src/components/auth/auth-redirect.tsx +11 -3
  209. package/src/components/auth/email-verification-result-banner.tsx +1 -3
  210. package/src/components/auth/resend-verification-button.tsx +2 -10
  211. package/src/components/auth/wopr-wordmark.tsx +1 -3
  212. package/src/components/billing/add-payment-method-dialog.tsx +1 -2
  213. package/src/components/billing/affiliate-dashboard.tsx +4 -16
  214. package/src/components/billing/amount-selector.tsx +1 -3
  215. package/src/components/billing/auto-topup-card.tsx +2 -11
  216. package/src/components/billing/buy-credits-panel.tsx +4 -14
  217. package/src/components/billing/byok-callout.tsx +6 -8
  218. package/src/components/billing/confirmation-tracker.tsx +4 -14
  219. package/src/components/billing/credit-balance-badge.tsx +22 -0
  220. package/src/components/billing/credit-balance.tsx +3 -9
  221. package/src/components/billing/crypto-checkout.tsx +5 -24
  222. package/src/components/billing/degraded-state-banner.tsx +1 -3
  223. package/src/components/billing/deposit-view.tsx +301 -41
  224. package/src/components/billing/dividend-banner.tsx +1 -3
  225. package/src/components/billing/dividend-eligibility.tsx +3 -12
  226. package/src/components/billing/dividend-pool-stats.tsx +6 -20
  227. package/src/components/billing/first-dividend-dialog.tsx +2 -2
  228. package/src/components/billing/org-billing-page.tsx +8 -31
  229. package/src/components/billing/payment-method-picker.tsx +2 -10
  230. package/src/components/billing/suspension-banner.tsx +2 -7
  231. package/src/components/billing/transaction-history.tsx +10 -58
  232. package/src/components/billing/unified-checkout.tsx +547 -0
  233. package/src/components/bot-settings/backups-tab.tsx +9 -33
  234. package/src/components/bot-settings/bot-settings-client.tsx +32 -134
  235. package/src/components/bot-settings/resources-tab.tsx +2 -9
  236. package/src/components/bot-settings/storage-tab.tsx +19 -48
  237. package/src/components/bot-settings/vps-info-panel.tsx +3 -11
  238. package/src/components/bot-settings/vps-upgrade-card.tsx +3 -4
  239. package/src/components/brand-hydrator.tsx +13 -0
  240. package/src/components/channel-wizard/field-interactive.tsx +1 -3
  241. package/src/components/channel-wizard/field-qr.tsx +10 -39
  242. package/src/components/channel-wizard/step-renderer.tsx +5 -28
  243. package/src/components/channel-wizard/wizard.tsx +6 -31
  244. package/src/components/chat/chat-message.tsx +1 -4
  245. package/src/components/chat/chat-panel.tsx +4 -18
  246. package/src/components/chat/chat-widget.tsx +3 -14
  247. package/src/components/dashboard/command-center.tsx +15 -61
  248. package/src/components/fleet/update-settings-card.tsx +7 -23
  249. package/src/components/instance-update-banner.tsx +130 -0
  250. package/src/components/instances/friends-tab.test.tsx +2 -9
  251. package/src/components/instances/friends-tab.tsx +18 -74
  252. package/src/components/instances/update-available-badge.tsx +2 -11
  253. package/src/components/landing/hero.tsx +3 -9
  254. package/src/components/landing/landing-page.tsx +1 -3
  255. package/src/components/landing/portfolio-chart.tsx +4 -9
  256. package/src/components/landing/story-sections.tsx +1 -3
  257. package/src/components/landing/terminal-sequence.tsx +4 -17
  258. package/src/components/marketplace/empty-state.tsx +2 -6
  259. package/src/components/marketplace/first-visit-hero.tsx +1 -3
  260. package/src/components/marketplace/install-wizard.tsx +20 -77
  261. package/src/components/marketplace/marketplace-tabs.tsx +1 -4
  262. package/src/components/marketplace/plugin-card.tsx +2 -9
  263. package/src/components/marketplace/superpower-content.tsx +1 -3
  264. package/src/components/marketplace/terminal-search.tsx +2 -8
  265. package/src/components/oauth-buttons.tsx +29 -14
  266. package/src/components/observability/fleet-health.tsx +5 -18
  267. package/src/components/observability/health-overview.tsx +7 -20
  268. package/src/components/observability/logs-viewer.tsx +8 -32
  269. package/src/components/observability/metrics-dashboard.tsx +2 -15
  270. package/src/components/onboarding/fallback-setup.tsx +6 -25
  271. package/src/components/onboarding/setup-checklist.tsx +18 -51
  272. package/src/components/onboarding/step-superpowers.tsx +1 -4
  273. package/src/components/plugin-setup/setup-chat-panel.tsx +6 -22
  274. package/src/components/pricing/dividend-calculator.tsx +6 -12
  275. package/src/components/pricing/dividend-stats.tsx +5 -17
  276. package/src/components/pricing/pricing-page.tsx +17 -36
  277. package/src/components/settings/create-org-wizard.tsx +2 -5
  278. package/src/components/sidebar.tsx +7 -42
  279. package/src/components/sidecar-frame.tsx +78 -0
  280. package/src/components/status/status-page.tsx +6 -28
  281. package/src/components/ui/alert-dialog.tsx +8 -25
  282. package/src/components/ui/badge.tsx +2 -8
  283. package/src/components/ui/banner.tsx +1 -6
  284. package/src/components/ui/card.tsx +5 -24
  285. package/src/components/ui/checkbox.tsx +1 -5
  286. package/src/components/ui/collapsible.tsx +3 -8
  287. package/src/components/ui/dialog.tsx +4 -10
  288. package/src/components/ui/dropdown-menu.tsx +9 -18
  289. package/src/components/ui/form.tsx +2 -16
  290. package/src/components/ui/popover.tsx +3 -23
  291. package/src/components/ui/progress.tsx +1 -5
  292. package/src/components/ui/radio-group.tsx +3 -15
  293. package/src/components/ui/select.tsx +4 -17
  294. package/src/components/ui/sheet.tsx +5 -19
  295. package/src/components/ui/skeleton.tsx +1 -7
  296. package/src/components/ui/table.tsx +5 -22
  297. package/src/components/ui/tabs.tsx +3 -13
  298. package/src/components/ui/tooltip.tsx +1 -1
  299. package/src/components/unified-sidebar.tsx +493 -0
  300. package/src/hooks/__tests__/use-fleet-sse.test.ts +1 -4
  301. package/src/hooks/__tests__/use-save-queue.test.ts +2 -8
  302. package/src/hooks/use-credit-balance.ts +27 -0
  303. package/src/hooks/use-my-org-role.ts +1 -3
  304. package/src/hooks/use-plugin-registry.ts +8 -14
  305. package/src/hooks/use-plugin-setup-chat.ts +2 -5
  306. package/src/hooks/use-sidecar-bridge.tsx +148 -0
  307. package/src/hooks/use-webmcp.ts +1 -4
  308. package/src/lib/__tests__/admin-api.test.ts +1 -3
  309. package/src/lib/__tests__/api-bot-crud.test.ts +8 -18
  310. package/src/lib/__tests__/api-fetch.test.ts +4 -16
  311. package/src/lib/__tests__/org-billing-api.test.ts +1 -3
  312. package/src/lib/__tests__/pricing-data.test.ts +0 -8
  313. package/src/lib/__tests__/settings-api.test.ts +1 -3
  314. package/src/lib/admin-affiliate-api.ts +2 -7
  315. package/src/lib/admin-api.ts +6 -26
  316. package/src/lib/admin-incident-api.ts +11 -19
  317. package/src/lib/admin-marketplace-api.ts +1 -5
  318. package/src/lib/api-config.test.ts +5 -50
  319. package/src/lib/api.ts +143 -122
  320. package/src/lib/auth-client.ts +1 -2
  321. package/src/lib/bot-settings-data.ts +11 -36
  322. package/src/lib/brand-config.ts +56 -115
  323. package/src/lib/brand.ts +2 -15
  324. package/src/lib/chat/use-chat.ts +2 -7
  325. package/src/lib/cost-comparison-data.test.ts +1 -3
  326. package/src/lib/cost-comparison-data.ts +1 -4
  327. package/src/lib/errors.ts +1 -4
  328. package/src/lib/fetch-utils.test.ts +26 -9
  329. package/src/lib/fetch-utils.ts +40 -11
  330. package/src/lib/logger.ts +2 -0
  331. package/src/lib/marketplace-data.ts +3 -11
  332. package/src/lib/oauth-errors.ts +2 -4
  333. package/src/lib/onboarding-data.ts +3 -11
  334. package/src/lib/org-api.ts +2 -10
  335. package/src/lib/org-billing-api.ts +5 -19
  336. package/src/lib/plugin/tool-definitions.ts +1 -2
  337. package/src/lib/require-auth.ts +57 -0
  338. package/src/lib/settings-api.ts +1 -4
  339. package/src/lib/sidecar-routes.ts +43 -0
  340. package/src/lib/trpc-server.ts +49 -0
  341. package/src/lib/trpc-types.ts +4 -6
  342. package/src/lib/trpc.tsx +12 -4
  343. package/src/lib/validate-redirect-url.ts +1 -4
  344. package/src/lib/webmcp/marketplace-onboarding-tools.ts +6 -16
  345. package/src/lib/webmcp/register.ts +1 -4
  346. package/src/lib/webmcp/tools.ts +2 -9
  347. package/src/proxy.ts +35 -212
  348. package/src/types/missing-deps.d.ts +2 -8
  349. package/tsconfig.json +1 -8
  350. package/biome.json +0 -52
  351. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +0 -741
  352. package/src/__tests__/billing-byok-callout.test.tsx +0 -76
  353. package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +0 -112
@@ -164,20 +164,14 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
164
164
  {/* bg-white is intentional -- QR codes require white background for scanability */}
165
165
  <div className="h-40 w-40 rounded-sm bg-white p-3 min-[375px]:h-48 min-[375px]:w-48">
166
166
  {/* biome-ignore lint/performance/noImgElement: QR PNG is a base64 data URI from the API — next/image does not support data: URIs (cannot optimize, resize, or lazy-load inline blobs). Raw <img> is the only option here. */}
167
- <img
168
- src={qrPng}
169
- alt="Scan this QR code with your phone"
170
- className="h-full w-full"
171
- />
167
+ <img src={qrPng} alt="Scan this QR code with your phone" className="h-full w-full" />
172
168
  </div>
173
169
  </div>
174
170
 
175
171
  {/* Scanning indicator */}
176
172
  <div className="flex items-center gap-2">
177
173
  <div className="h-1.5 w-1.5 rounded-full bg-terminal motion-safe:animate-[pulse-dot_2s_ease-in-out_infinite]" />
178
- <span className="text-xs font-medium uppercase tracking-wider text-terminal">
179
- WAITING FOR SCAN
180
- </span>
174
+ <span className="text-xs font-medium uppercase tracking-wider text-terminal">WAITING FOR SCAN</span>
181
175
  </div>
182
176
 
183
177
  {/* Instructions */}
@@ -186,9 +180,7 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
186
180
  </p>
187
181
 
188
182
  {/* Countdown */}
189
- <span className={`text-xs tabular-nums ${countdownColor()}`}>
190
- Expires in {secondsLeft}s
191
- </span>
183
+ <span className={`text-xs tabular-nums ${countdownColor()}`}>Expires in {secondsLeft}s</span>
192
184
  </div>
193
185
  )}
194
186
 
@@ -215,21 +207,13 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
215
207
  </div>
216
208
  </div>
217
209
 
218
- <span className="text-xs font-medium uppercase tracking-wider text-amber-500">
219
- QR CODE EXPIRED
220
- </span>
210
+ <span className="text-xs font-medium uppercase tracking-wider text-amber-500">QR CODE EXPIRED</span>
221
211
 
222
212
  <p className="text-center text-sm text-muted-foreground">
223
213
  The code expired. Tap below to generate a fresh one.
224
214
  </p>
225
215
 
226
- <Button
227
- type="button"
228
- variant="terminal"
229
- size="sm"
230
- className="h-10 px-4"
231
- onClick={handleRefresh}
232
- >
216
+ <Button type="button" variant="terminal" size="sm" className="h-10 px-4" onClick={handleRefresh}>
233
217
  <RefreshCw className="size-3.5" />
234
218
  Generate New Code
235
219
  </Button>
@@ -243,9 +227,7 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
243
227
  <Check className="size-16 text-emerald-500" />
244
228
  </div>
245
229
 
246
- <span className="text-xs font-medium uppercase tracking-wider text-emerald-500">
247
- LINKED SUCCESSFULLY
248
- </span>
230
+ <span className="text-xs font-medium uppercase tracking-wider text-emerald-500">LINKED SUCCESSFULLY</span>
249
231
 
250
232
  <p className="text-center text-sm text-muted-foreground">
251
233
  {field.label ? `${field.label} connected` : "Connected"}. You can continue setup.
@@ -258,22 +240,13 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
258
240
  <div className="flex flex-col items-center gap-4">
259
241
  <AlertTriangle className="size-10 text-destructive" />
260
242
 
261
- <span className="text-xs font-medium uppercase tracking-wider text-destructive">
262
- CONNECTION ERROR
263
- </span>
243
+ <span className="text-xs font-medium uppercase tracking-wider text-destructive">CONNECTION ERROR</span>
264
244
 
265
245
  <p className="max-w-[280px] text-center text-sm text-muted-foreground">
266
- {errorMsg ||
267
- "Could not reach the server. Check that your bot is running and try again."}
246
+ {errorMsg || "Could not reach the server. Check that your bot is running and try again."}
268
247
  </p>
269
248
 
270
- <Button
271
- type="button"
272
- variant="terminal"
273
- size="sm"
274
- className="h-10 px-4"
275
- onClick={handleRefresh}
276
- >
249
+ <Button type="button" variant="terminal" size="sm" className="h-10 px-4" onClick={handleRefresh}>
277
250
  <RefreshCw className="size-3.5" />
278
251
  Try Again
279
252
  </Button>
@@ -285,9 +258,7 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
285
258
  <div className="flex flex-col items-center gap-4">
286
259
  <WifiOff className="size-10 text-muted-foreground" />
287
260
 
288
- <span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
289
- BOT OFFLINE
290
- </span>
261
+ <span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">BOT OFFLINE</span>
291
262
 
292
263
  <p className="max-w-[280px] text-center text-sm text-muted-foreground">
293
264
  Your bot is currently offline. Start it from the fleet dashboard to link your account.
@@ -24,34 +24,13 @@ function renderField(
24
24
  ) {
25
25
  switch (field.setupFlow) {
26
26
  case "oauth":
27
- return (
28
- <FieldOAuth key={field.key} field={field} value={value} onChange={onChange} error={error} />
29
- );
27
+ return <FieldOAuth key={field.key} field={field} value={value} onChange={onChange} error={error} />;
30
28
  case "qr":
31
- return (
32
- <FieldQR
33
- key={field.key}
34
- field={field}
35
- value={value}
36
- onChange={onChange}
37
- error={error}
38
- botId={botId}
39
- />
40
- );
29
+ return <FieldQR key={field.key} field={field} value={value} onChange={onChange} error={error} botId={botId} />;
41
30
  case "interactive":
42
- return (
43
- <FieldInteractive
44
- key={field.key}
45
- field={field}
46
- value={value}
47
- onChange={onChange}
48
- error={error}
49
- />
50
- );
31
+ return <FieldInteractive key={field.key} field={field} value={value} onChange={onChange} error={error} />;
51
32
  default:
52
- return (
53
- <FieldPaste key={field.key} field={field} value={value} onChange={onChange} error={error} />
54
- );
33
+ return <FieldPaste key={field.key} field={field} value={value} onChange={onChange} error={error} />;
55
34
  }
56
35
  }
57
36
 
@@ -93,9 +72,7 @@ export function StepRenderer({ step, values, errors, onChange, botId }: StepRend
93
72
 
94
73
  {hasFields && (
95
74
  <div className="space-y-4">
96
- {step.fields.map((field) =>
97
- renderField(field, values[field.key] || "", onChange, errors[field.key], botId),
98
- )}
75
+ {step.fields.map((field) => renderField(field, values[field.key] || "", onChange, errors[field.key], botId))}
99
76
  </div>
100
77
  )}
101
78
  </div>
@@ -3,14 +3,7 @@
3
3
  import { useCallback, useState } from "react";
4
4
  import { z } from "zod";
5
5
  import { Button } from "@/components/ui/button";
6
- import {
7
- Card,
8
- CardContent,
9
- CardDescription,
10
- CardFooter,
11
- CardHeader,
12
- CardTitle,
13
- } from "@/components/ui/card";
6
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
14
7
  import { Progress } from "@/components/ui/progress";
15
8
  import { testChannelConnection } from "@/lib/api";
16
9
  import type { ChannelManifest } from "@/lib/channel-manifests";
@@ -55,10 +48,7 @@ export function Wizard({ manifest, onComplete, onCancel, submitting, botId }: Wi
55
48
  strSchema = strSchema.min(1, `${field.label} is required`);
56
49
  }
57
50
  if (field.validation?.pattern) {
58
- strSchema = strSchema.regex(
59
- new RegExp(field.validation.pattern),
60
- field.validation.message || "Invalid format",
61
- );
51
+ strSchema = strSchema.regex(new RegExp(field.validation.pattern), field.validation.message || "Invalid format");
62
52
  }
63
53
  shape[field.key] = field.required ? strSchema : strSchema.optional().or(z.literal(""));
64
54
  }
@@ -149,31 +139,16 @@ export function Wizard({ manifest, onComplete, onCancel, submitting, botId }: Wi
149
139
  </CardHeader>
150
140
 
151
141
  <CardContent>
152
- <StepRenderer
153
- step={step}
154
- values={values}
155
- errors={errors}
156
- onChange={handleChange}
157
- botId={botId}
158
- />
142
+ <StepRenderer step={step} values={values} errors={errors} onChange={handleChange} botId={botId} />
159
143
 
160
144
  {manifest.connectionTest && isLastStep && (
161
145
  <div className="mt-6 flex flex-col items-center gap-2">
162
- <Button
163
- type="button"
164
- variant="outline"
165
- onClick={handleTestConnection}
166
- disabled={testing}
167
- >
146
+ <Button type="button" variant="outline" onClick={handleTestConnection} disabled={testing}>
168
147
  {testing ? "Testing..." : manifest.connectionTest.label}
169
148
  </Button>
170
- {testResult === "success" && (
171
- <p className="text-sm text-emerald-500">Connection successful</p>
172
- )}
149
+ {testResult === "success" && <p className="text-sm text-emerald-500">Connection successful</p>}
173
150
  {testResult === "failure" && (
174
- <p className="text-sm text-destructive">
175
- {testError || "Connection failed. Check your settings."}
176
- </p>
151
+ <p className="text-sm text-destructive">{testError || "Connection failed. Check your settings."}</p>
177
152
  )}
178
153
  </div>
179
154
  )}
@@ -18,10 +18,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
18
18
  const isUser = message.role === "user";
19
19
 
20
20
  return (
21
- <div
22
- className={`flex ${isUser ? "justify-end" : "justify-start"}`}
23
- data-testid={`chat-message-${message.role}`}
24
- >
21
+ <div className={`flex ${isUser ? "justify-end" : "justify-start"}`} data-testid={`chat-message-${message.role}`}>
25
22
  <div
26
23
  className={`max-w-[80%] rounded-lg px-3 py-2 ${
27
24
  isUser
@@ -36,15 +36,7 @@ const fullscreenVariants = {
36
36
  exit: { opacity: 0, transition: { duration: 0.15 } },
37
37
  };
38
38
 
39
- export function ChatPanel({
40
- messages,
41
- mode,
42
- isConnected,
43
- isTyping,
44
- onSend,
45
- onClose,
46
- onFullscreen,
47
- }: ChatPanelProps) {
39
+ export function ChatPanel({ messages, mode, isConnected, isTyping, onSend, onClose, onFullscreen }: ChatPanelProps) {
48
40
  const messagesEndRef = useRef<HTMLDivElement>(null);
49
41
  const isFullscreen = mode === "fullscreen";
50
42
 
@@ -81,9 +73,7 @@ export function ChatPanel({
81
73
  className={`h-2 w-2 rounded-full inline-block ${isConnected ? "bg-terminal" : "bg-destructive"}`}
82
74
  aria-label={isConnected ? "Connected" : "Disconnected"}
83
75
  />
84
- <span className="text-xs font-mono uppercase tracking-wider text-muted-foreground">
85
- {brandName()}
86
- </span>
76
+ <span className="text-xs font-mono uppercase tracking-wider text-muted-foreground">{brandName()}</span>
87
77
  </div>
88
78
  <div className="flex items-center gap-1">
89
79
  {!isFullscreen && (
@@ -112,13 +102,9 @@ export function ChatPanel({
112
102
  </div>
113
103
 
114
104
  {/* Messages */}
115
- <div
116
- className={`flex-1 overflow-y-auto px-4 py-3 space-y-2 ${isFullscreen ? "max-w-2xl mx-auto w-full" : ""}`}
117
- >
105
+ <div className={`flex-1 overflow-y-auto px-4 py-3 space-y-2 ${isFullscreen ? "max-w-2xl mx-auto w-full" : ""}`}>
118
106
  {messages.length === 0 && !isConnected && (
119
- <p className="text-center text-xs text-muted-foreground animate-ellipsis">
120
- Connecting to {brandName()}
121
- </p>
107
+ <p className="text-center text-xs text-muted-foreground animate-ellipsis">Connecting to {brandName()}</p>
122
108
  )}
123
109
  {messages.map((msg) => (
124
110
  <ChatMessage key={msg.id} message={msg} />
@@ -6,23 +6,12 @@ import { AmbientDot } from "./ambient-dot";
6
6
  import { ChatPanel } from "./chat-panel";
7
7
 
8
8
  export function ChatWidget() {
9
- const {
10
- messages,
11
- mode,
12
- isConnected,
13
- isTyping,
14
- hasUnread,
15
- expand,
16
- collapse,
17
- fullscreen,
18
- sendMessage,
19
- } = useChatContext();
9
+ const { messages, mode, isConnected, isTyping, hasUnread, expand, collapse, fullscreen, sendMessage } =
10
+ useChatContext();
20
11
 
21
12
  return (
22
13
  <>
23
- <AnimatePresence>
24
- {mode === "collapsed" && <AmbientDot hasUnread={hasUnread} onClick={expand} />}
25
- </AnimatePresence>
14
+ <AnimatePresence>{mode === "collapsed" && <AmbientDot hasUnread={hasUnread} onClick={expand} />}</AnimatePresence>
26
15
  <AnimatePresence>
27
16
  {(mode === "expanded" || mode === "fullscreen") && (
28
17
  <ChatPanel
@@ -9,19 +9,8 @@ import { Button } from "@/components/ui/button";
9
9
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10
10
  import { useFleetSSE } from "@/hooks/use-fleet-sse";
11
11
  import { usePaginationParams } from "@/hooks/use-pagination-params";
12
- import type {
13
- ActivityEvent,
14
- BotStatusResponse,
15
- DividendWalletStats,
16
- FleetInstance,
17
- FleetResources,
18
- } from "@/lib/api";
19
- import {
20
- getActivityFeed,
21
- getDividendStats,
22
- getFleetResources,
23
- mapBotStatusToFleetInstance,
24
- } from "@/lib/api";
12
+ import type { ActivityEvent, BotStatusResponse, DividendWalletStats, FleetInstance, FleetResources } from "@/lib/api";
13
+ import { getActivityFeed, getDividendStats, getFleetResources, mapBotStatusToFleetInstance } from "@/lib/api";
25
14
  import { productName } from "@/lib/brand-config";
26
15
  import { toUserMessage } from "@/lib/errors";
27
16
  import { formatRelativeTime } from "@/lib/format";
@@ -32,9 +21,7 @@ import { cn } from "@/lib/utils";
32
21
  function computeFleetStats(instances: FleetInstance[], resources: FleetResources | null) {
33
22
  const running = instances.filter((i) => i.status === "running").length;
34
23
  const stopped = instances.filter((i) => i.status === "stopped").length;
35
- const degraded = instances.filter(
36
- (i) => i.health === "degraded" || i.health === "unhealthy",
37
- ).length;
24
+ const degraded = instances.filter((i) => i.health === "degraded" || i.health === "unhealthy").length;
38
25
  const totalCpu = resources?.totalCpuPercent ?? 0;
39
26
  const totalMemory = resources?.totalMemoryMb ?? 0;
40
27
  const memoryCapacity = resources?.memoryCapacityMb ?? 2048;
@@ -236,12 +223,7 @@ export function CommandCenter() {
236
223
  className="flex items-center justify-between rounded-md border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-400"
237
224
  >
238
225
  <span>{error}</span>
239
- <Button
240
- data-onboarding-id="dashboard.retry"
241
- variant="ghost"
242
- size="sm"
243
- onClick={() => loadNonFleet()}
244
- >
226
+ <Button data-onboarding-id="dashboard.retry" variant="ghost" size="sm" onClick={() => loadNonFleet()}>
245
227
  Retry
246
228
  </Button>
247
229
  </div>
@@ -249,10 +231,7 @@ export function CommandCenter() {
249
231
 
250
232
  {/* Fleet Summary Cards */}
251
233
  <motion.div
252
- className={cn(
253
- "grid gap-4 sm:grid-cols-2",
254
- dividendStats ? "lg:grid-cols-5" : "lg:grid-cols-4",
255
- )}
234
+ className={cn("grid gap-4 sm:grid-cols-2", dividendStats ? "lg:grid-cols-5" : "lg:grid-cols-4")}
256
235
  variants={staggerContainer}
257
236
  initial="hidden"
258
237
  animate="show"
@@ -268,9 +247,7 @@ export function CommandCenter() {
268
247
  <p className="mt-2 text-3xl font-bold tabular-nums" data-testid="running-count">
269
248
  {loading ? "--" : <CountUp value={stats.running} />}
270
249
  </p>
271
- <p className="text-xs text-muted-foreground">
272
- {stats.running === 1 ? "instance" : "instances"}
273
- </p>
250
+ <p className="text-xs text-muted-foreground">{stats.running === 1 ? "instance" : "instances"}</p>
274
251
  </CardContent>
275
252
  </Card>
276
253
  </motion.div>
@@ -286,9 +263,7 @@ export function CommandCenter() {
286
263
  <p className="mt-2 text-3xl font-bold tabular-nums" data-testid="stopped-count">
287
264
  {loading ? "--" : <CountUp value={stats.stopped} />}
288
265
  </p>
289
- <p className="text-xs text-muted-foreground">
290
- {stats.stopped === 1 ? "instance" : "instances"}
291
- </p>
266
+ <p className="text-xs text-muted-foreground">{stats.stopped === 1 ? "instance" : "instances"}</p>
292
267
  </CardContent>
293
268
  </Card>
294
269
  </motion.div>
@@ -308,9 +283,7 @@ export function CommandCenter() {
308
283
  <p className="mt-2 text-3xl font-bold tabular-nums" data-testid="degraded-count">
309
284
  {loading ? "--" : <CountUp value={stats.degraded} />}
310
285
  </p>
311
- <p className="text-xs text-muted-foreground">
312
- {stats.degraded > 0 ? "need attention" : "all clear"}
313
- </p>
286
+ <p className="text-xs text-muted-foreground">{stats.degraded > 0 ? "need attention" : "all clear"}</p>
314
287
  </CardContent>
315
288
  </Card>
316
289
  </motion.div>
@@ -349,9 +322,7 @@ export function CommandCenter() {
349
322
  className="h-full rounded-full bg-primary"
350
323
  initial={{ width: "0%" }}
351
324
  animate={{
352
- width: loading
353
- ? "0%"
354
- : `${Math.min(100, (stats.totalMemory / stats.memoryCapacity) * 100)}%`,
325
+ width: loading ? "0%" : `${Math.min(100, (stats.totalMemory / stats.memoryCapacity) * 100)}%`,
355
326
  }}
356
327
  transition={{ duration: 0.8, ease: "easeOut", delay: 0.3 }}
357
328
  />
@@ -372,10 +343,7 @@ export function CommandCenter() {
372
343
  <p className="text-sm text-muted-foreground">Today&apos;s Dividend</p>
373
344
  <TrendingUpIcon className="size-4 text-terminal" />
374
345
  </div>
375
- <p
376
- className="mt-2 text-3xl font-bold tabular-nums text-terminal"
377
- data-testid="dividend-amount"
378
- >
346
+ <p className="mt-2 text-3xl font-bold tabular-nums text-terminal" data-testid="dividend-amount">
379
347
  {loading ? "--" : formatCreditStandard((dividendStats?.perUserCents ?? 0) / 100)}
380
348
  </p>
381
349
  <p className="text-xs text-muted-foreground">
@@ -407,9 +375,7 @@ export function CommandCenter() {
407
375
  ) : activity.length === 0 ? (
408
376
  <div className="flex flex-col items-center gap-2 py-8 text-center">
409
377
  <p className="font-mono text-sm text-terminal">&gt; STANDING BY</p>
410
- <p className="font-mono text-xs text-muted-foreground">
411
- NO EVENTS LOGGED. AWAITING ACTIVITY.
412
- </p>
378
+ <p className="font-mono text-xs text-muted-foreground">NO EVENTS LOGGED. AWAITING ACTIVITY.</p>
413
379
  </div>
414
380
  ) : (
415
381
  <motion.div
@@ -475,11 +441,7 @@ export function CommandCenter() {
475
441
  <motion.div
476
442
  className="h-full rounded-sm border border-dashed border-terminal/20"
477
443
  animate={{
478
- borderColor: [
479
- "hsl(var(--terminal) / 0.2)",
480
- "hsl(var(--terminal) / 0.5)",
481
- "hsl(var(--terminal) / 0.2)",
482
- ],
444
+ borderColor: ["hsl(var(--terminal) / 0.2)", "hsl(var(--terminal) / 0.5)", "hsl(var(--terminal) / 0.2)"],
483
445
  }}
484
446
  transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
485
447
  >
@@ -497,9 +459,7 @@ export function CommandCenter() {
497
459
  <Plus size={24} />
498
460
  </motion.div>
499
461
  <p className="font-semibold">Add another {productName()}</p>
500
- <p className="mt-1 text-xs text-muted-foreground">
501
- Name it. Teach it. Give it superpowers.
502
- </p>
462
+ <p className="mt-1 text-xs text-muted-foreground">Name it. Teach it. Give it superpowers.</p>
503
463
  </CardContent>
504
464
  </Card>
505
465
  </motion.div>
@@ -511,16 +471,10 @@ export function CommandCenter() {
511
471
  {fleetTotal > FLEET_PAGE_SIZE && (
512
472
  <div className="flex items-center justify-between text-xs text-muted-foreground font-mono">
513
473
  <span>
514
- Showing {fleetStart + 1}-{Math.min(fleetStart + FLEET_PAGE_SIZE, fleetTotal)} of{" "}
515
- {fleetTotal}
474
+ Showing {fleetStart + 1}-{Math.min(fleetStart + FLEET_PAGE_SIZE, fleetTotal)} of {fleetTotal}
516
475
  </span>
517
476
  <div className="flex gap-2">
518
- <Button
519
- variant="ghost"
520
- size="xs"
521
- disabled={fleetPage <= 1}
522
- onClick={() => setFleetPage(fleetPage - 1)}
523
- >
477
+ <Button variant="ghost" size="xs" disabled={fleetPage <= 1} onClick={() => setFleetPage(fleetPage - 1)}>
524
478
  Previous
525
479
  </Button>
526
480
  <Button
@@ -5,20 +5,13 @@ import { useState } from "react";
5
5
  import { toast } from "sonner";
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
7
  import { Label } from "@/components/ui/label";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "@/components/ui/select";
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
15
9
  import { Switch } from "@/components/ui/switch";
16
10
  import { useTenant } from "@/lib/tenant-context";
17
11
  import { trpc } from "@/lib/trpc";
18
12
 
19
13
  const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => {
20
- const label =
21
- i === 0 ? "12:00 AM" : i < 12 ? `${i}:00 AM` : i === 12 ? "12:00 PM" : `${i - 12}:00 PM`;
14
+ const label = i === 0 ? "12:00 AM" : i < 12 ? `${i}:00 AM` : i === 12 ? "12:00 PM" : `${i - 12}:00 PM`;
22
15
  return { value: String(i), label };
23
16
  });
24
17
 
@@ -26,10 +19,7 @@ export function UpdateSettingsCard() {
26
19
  const { activeTenantId: tenantId } = useTenant();
27
20
  const [saving, setSaving] = useState(false);
28
21
 
29
- const configQuery = trpc.fleetUpdateConfig.getUpdateConfig.useQuery(
30
- { tenantId },
31
- { enabled: !!tenantId },
32
- );
22
+ const configQuery = trpc.fleetUpdateConfig.getUpdateConfig.useQuery({ tenantId }, { enabled: !!tenantId });
33
23
 
34
24
  const setConfigMutation = trpc.fleetUpdateConfig.setUpdateConfig.useMutation();
35
25
 
@@ -82,8 +72,8 @@ export function UpdateSettingsCard() {
82
72
  Auto-Update Settings
83
73
  </CardTitle>
84
74
  <CardDescription>
85
- Control how your fleet receives updates. Auto mode applies updates during your preferred
86
- maintenance window. Manual mode shows an update badge — you choose when to apply.
75
+ Control how your fleet receives updates. Auto mode applies updates during your preferred maintenance window.
76
+ Manual mode shows an update badge — you choose when to apply.
87
77
  </CardDescription>
88
78
  </CardHeader>
89
79
  <CardContent className="space-y-6">
@@ -129,15 +119,9 @@ export function UpdateSettingsCard() {
129
119
  <Clock className="h-4 w-4 text-muted-foreground" />
130
120
  Maintenance Window
131
121
  </Label>
132
- <p className="text-sm text-muted-foreground">
133
- Preferred hour for applying updates (UTC)
134
- </p>
122
+ <p className="text-sm text-muted-foreground">Preferred hour for applying updates (UTC)</p>
135
123
  </div>
136
- <Select
137
- value={String(preferredHourUtc)}
138
- onValueChange={handleHourChange}
139
- disabled={saving}
140
- >
124
+ <Select value={String(preferredHourUtc)} onValueChange={handleHourChange} disabled={saving}>
141
125
  <SelectTrigger className="w-[140px]">
142
126
  <SelectValue />
143
127
  </SelectTrigger>
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { controlInstance, type InstanceVersionCheck, instanceVersionCheck } from "@/lib/api";
6
+ import { trpcVanilla } from "@/lib/trpc";
7
+
8
+ type Status = "idle" | "rolling" | "done";
9
+
10
+ /**
11
+ * Banner that appears when the user's sidecar container is running an
12
+ * older image than the one currently pulled on the host node. Offers an
13
+ * opt-in "Update now" button that triggers a fleet.controlInstance roll.
14
+ *
15
+ * Polls every 60s. During roll the banner shows "Updating…" and expects
16
+ * a brief iframe/sidebar reconnect; on success it reloads the page so
17
+ * the fresh container serves the iframe from its new image.
18
+ */
19
+ export function InstanceUpdateBanner() {
20
+ const [instanceId, setInstanceId] = useState<string | null>(null);
21
+ const [check, setCheck] = useState<InstanceVersionCheck | null>(null);
22
+ const [status, setStatus] = useState<Status>("idle");
23
+ const [dismissed, setDismissed] = useState(false);
24
+ const reloadTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
25
+
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+ // Go direct to the raw tRPC response so we can read platformInstanceId
29
+ // (the bot_instances UUID). `listInstances()` in api.ts maps to a
30
+ // client-friendly shape but reads `bot.id` which is the docker
31
+ // container hash in the success path — not what the fleet APIs want.
32
+ (async () => {
33
+ try {
34
+ const data = (await trpcVanilla.fleet.listInstances.query(undefined)) as {
35
+ bots?: Array<{ platformInstanceId?: string }>;
36
+ };
37
+ // The hosted shell currently provisions exactly one sidecar per
38
+ // user, so bots[0] is the workspace the shell is backing. If
39
+ // multi-instance support lands, pick the active instance here.
40
+ if (!cancelled) setInstanceId(data.bots?.[0]?.platformInstanceId ?? null);
41
+ } catch {
42
+ if (!cancelled) setInstanceId(null);
43
+ }
44
+ })();
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, []);
49
+
50
+ // Clear any in-flight reload timer if the component unmounts, to avoid
51
+ // a setState on an unmounted component and a surprise reload firing
52
+ // after navigation.
53
+ useEffect(() => {
54
+ return () => {
55
+ if (reloadTimeoutRef.current) {
56
+ clearTimeout(reloadTimeoutRef.current);
57
+ reloadTimeoutRef.current = null;
58
+ }
59
+ };
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ if (!instanceId) return;
64
+ let cancelled = false;
65
+ async function poll() {
66
+ if (!instanceId) return;
67
+ try {
68
+ const result = await instanceVersionCheck(instanceId);
69
+ if (!cancelled) setCheck(result);
70
+ } catch {
71
+ // Transient errors are fine — we'll retry on the next tick.
72
+ }
73
+ }
74
+ void poll();
75
+ const handle = window.setInterval(poll, 60_000);
76
+ return () => {
77
+ cancelled = true;
78
+ window.clearInterval(handle);
79
+ };
80
+ }, [instanceId]);
81
+
82
+ const onUpdate = useCallback(async () => {
83
+ if (!instanceId) return;
84
+ setStatus("rolling");
85
+ try {
86
+ await controlInstance(instanceId, "roll");
87
+ // Give the container ~25s to come back up before reloading. A tighter
88
+ // loop that polls health would be nicer, but reload is simpler and
89
+ // matches how the shell already recovers from routeChanged.
90
+ reloadTimeoutRef.current = setTimeout(() => {
91
+ setStatus("done");
92
+ window.location.reload();
93
+ }, 25_000);
94
+ } catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ toast.error(`Update failed: ${msg}`);
97
+ setStatus("idle");
98
+ }
99
+ }, [instanceId]);
100
+
101
+ if (dismissed) return null;
102
+ if (!check) return null;
103
+ if (check.upToDate) return null;
104
+
105
+ return (
106
+ <div className="flex items-center gap-3 border-b border-amber-500/30 bg-amber-500/10 px-4 py-2 text-sm text-amber-100">
107
+ <span className="font-medium">New version available</span>
108
+ <span className="text-amber-100/70">Your workspace will briefly disconnect while we swap it in.</span>
109
+ <div className="ml-auto flex items-center gap-2">
110
+ <button
111
+ type="button"
112
+ onClick={onUpdate}
113
+ disabled={status !== "idle"}
114
+ className="rounded-md bg-amber-500 px-3 py-1 text-xs font-medium text-amber-950 transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-60"
115
+ >
116
+ {status === "rolling" ? "Updating…" : status === "done" ? "Reloading…" : "Update now"}
117
+ </button>
118
+ <button
119
+ type="button"
120
+ onClick={() => setDismissed(true)}
121
+ disabled={status !== "idle"}
122
+ className="rounded-md px-2 py-1 text-xs text-amber-100/70 hover:bg-amber-500/10 hover:text-amber-50 disabled:cursor-not-allowed"
123
+ aria-label="Dismiss update banner"
124
+ >
125
+ Later
126
+ </button>
127
+ </div>
128
+ </div>
129
+ );
130
+ }