@wopr-network/platform-ui-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (543) hide show
  1. package/.env.paperclip +18 -0
  2. package/.env.wopr +18 -0
  3. package/README.md +36 -0
  4. package/biome.json +52 -0
  5. package/next.config.ts +45 -0
  6. package/package.json +84 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/window.svg +1 -0
  11. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
  12. package/src/__tests__/account-page-redirect.test.tsx +73 -0
  13. package/src/__tests__/account-switcher.test.tsx +85 -0
  14. package/src/__tests__/activity-page.test.tsx +176 -0
  15. package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
  16. package/src/__tests__/admin-api.test.ts +244 -0
  17. package/src/__tests__/admin-gpu-api.test.ts +188 -0
  18. package/src/__tests__/admin-guard.test.tsx +79 -0
  19. package/src/__tests__/admin-marketplace-api.test.ts +179 -0
  20. package/src/__tests__/admin-middleware.test.ts +157 -0
  21. package/src/__tests__/admin-tenant-table.test.tsx +95 -0
  22. package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
  23. package/src/__tests__/api-401-redirect.test.ts +78 -0
  24. package/src/__tests__/api-client.test.ts +316 -0
  25. package/src/__tests__/api-config.test.ts +89 -0
  26. package/src/__tests__/api-control-instance.test.ts +69 -0
  27. package/src/__tests__/api-fleet-resources.test.ts +52 -0
  28. package/src/__tests__/api-fleet-trpc.test.ts +252 -0
  29. package/src/__tests__/api-get-instance-config.test.ts +41 -0
  30. package/src/__tests__/api-null-guards.test.ts +244 -0
  31. package/src/__tests__/api-rename-instance.test.ts +60 -0
  32. package/src/__tests__/api-update-instance-config.test.ts +60 -0
  33. package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
  34. package/src/__tests__/auth-client.test.ts +87 -0
  35. package/src/__tests__/auth-password-reset.test.tsx +435 -0
  36. package/src/__tests__/auth-redirect.test.tsx +60 -0
  37. package/src/__tests__/auth.test.tsx +269 -0
  38. package/src/__tests__/auto-topup-card.test.tsx +257 -0
  39. package/src/__tests__/backups-tab.test.tsx +221 -0
  40. package/src/__tests__/billing-byok-callout.test.tsx +76 -0
  41. package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
  42. package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
  43. package/src/__tests__/billing.test.tsx +509 -0
  44. package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
  45. package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
  46. package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
  47. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
  48. package/src/__tests__/bot-settings-data-control.test.ts +49 -0
  49. package/src/__tests__/bot-settings-restart.test.tsx +149 -0
  50. package/src/__tests__/bot-settings.test.tsx +678 -0
  51. package/src/__tests__/brand.test.ts +335 -0
  52. package/src/__tests__/buy-credits-panel.test.tsx +249 -0
  53. package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
  54. package/src/__tests__/capability-conflicts.test.ts +88 -0
  55. package/src/__tests__/capability-resolver.test.tsx +173 -0
  56. package/src/__tests__/changeset-detail.test.tsx +156 -0
  57. package/src/__tests__/channel-setup-logger.test.ts +22 -0
  58. package/src/__tests__/channel-setup-toast.test.tsx +60 -0
  59. package/src/__tests__/channel-wizard.test.tsx +505 -0
  60. package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
  61. package/src/__tests__/chat/chat-input.test.tsx +78 -0
  62. package/src/__tests__/chat/chat-message.test.tsx +45 -0
  63. package/src/__tests__/chat/chat-panel.test.tsx +111 -0
  64. package/src/__tests__/chat/chat-widget.test.tsx +82 -0
  65. package/src/__tests__/chat-store.test.ts +87 -0
  66. package/src/__tests__/command-center.test.tsx +246 -0
  67. package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
  68. package/src/__tests__/coupon-input.test.tsx +119 -0
  69. package/src/__tests__/create-instance.test.tsx +96 -0
  70. package/src/__tests__/create-org-wizard.test.tsx +200 -0
  71. package/src/__tests__/credit-balance.test.tsx +103 -0
  72. package/src/__tests__/credits.test.tsx +376 -0
  73. package/src/__tests__/csrf-middleware.test.ts +198 -0
  74. package/src/__tests__/degraded-state-banner.test.tsx +130 -0
  75. package/src/__tests__/dividend-calculator.test.tsx +20 -0
  76. package/src/__tests__/dividend-stats.test.tsx +64 -0
  77. package/src/__tests__/dividend.test.tsx +169 -0
  78. package/src/__tests__/dockerfile.test.ts +110 -0
  79. package/src/__tests__/email-verification-banner.test.tsx +64 -0
  80. package/src/__tests__/env-example.test.ts +25 -0
  81. package/src/__tests__/error-boundaries.test.tsx +64 -0
  82. package/src/__tests__/fetch-pricing.test.ts +121 -0
  83. package/src/__tests__/field-oauth.test.tsx +302 -0
  84. package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
  85. package/src/__tests__/fixtures/mock-manifests.ts +24 -0
  86. package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
  87. package/src/__tests__/fleet-health-update.test.tsx +83 -0
  88. package/src/__tests__/format-credit.test.ts +58 -0
  89. package/src/__tests__/gpu-dashboard.test.tsx +236 -0
  90. package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
  91. package/src/__tests__/instance-detail.test.tsx +571 -0
  92. package/src/__tests__/instance-list.test.tsx +230 -0
  93. package/src/__tests__/landing-hero.test.tsx +27 -0
  94. package/src/__tests__/landing-nav.test.tsx +24 -0
  95. package/src/__tests__/layout-snapshots.test.tsx +167 -0
  96. package/src/__tests__/logger.test.ts +54 -0
  97. package/src/__tests__/login-page-redirect.test.tsx +142 -0
  98. package/src/__tests__/manifest-validation.test.ts +126 -0
  99. package/src/__tests__/marketplace-admin.test.tsx +151 -0
  100. package/src/__tests__/marketplace.test.tsx +609 -0
  101. package/src/__tests__/merge-api-rates.test.ts +70 -0
  102. package/src/__tests__/middleware.test.ts +690 -0
  103. package/src/__tests__/network-page.test.tsx +100 -0
  104. package/src/__tests__/next-config-headers.test.ts +28 -0
  105. package/src/__tests__/not-found.test.tsx +26 -0
  106. package/src/__tests__/notifications.test.tsx +128 -0
  107. package/src/__tests__/oauth-buttons.test.tsx +101 -0
  108. package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
  109. package/src/__tests__/observability.test.tsx +541 -0
  110. package/src/__tests__/onboarding-data.test.ts +363 -0
  111. package/src/__tests__/onboarding-page.test.tsx +113 -0
  112. package/src/__tests__/onboarding-store.test.ts +121 -0
  113. package/src/__tests__/org-billing-api.test.tsx +70 -0
  114. package/src/__tests__/org-billing-null-guards.test.ts +64 -0
  115. package/src/__tests__/org-billing-page.test.tsx +124 -0
  116. package/src/__tests__/plugin-definition.test.ts +43 -0
  117. package/src/__tests__/plugin-install-flow.test.tsx +535 -0
  118. package/src/__tests__/plugin-registry.test.tsx +475 -0
  119. package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
  120. package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
  121. package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
  122. package/src/__tests__/plugin-tool-sync.test.ts +59 -0
  123. package/src/__tests__/portfolio-chart.test.tsx +24 -0
  124. package/src/__tests__/pricing.test.tsx +107 -0
  125. package/src/__tests__/promotion-form.test.tsx +180 -0
  126. package/src/__tests__/promotions-list.test.tsx +194 -0
  127. package/src/__tests__/provider-key-api.test.ts +134 -0
  128. package/src/__tests__/resend-verification-button.test.tsx +104 -0
  129. package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
  130. package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
  131. package/src/__tests__/settings.test.tsx +937 -0
  132. package/src/__tests__/setup-checklist.test.tsx +274 -0
  133. package/src/__tests__/setup.ts +82 -0
  134. package/src/__tests__/smoke.test.tsx +10 -0
  135. package/src/__tests__/snapshot-api.test.ts +104 -0
  136. package/src/__tests__/status-api.test.ts +46 -0
  137. package/src/__tests__/status-badge.test.tsx +33 -0
  138. package/src/__tests__/status-colors.test.ts +83 -0
  139. package/src/__tests__/status-page.test.tsx +86 -0
  140. package/src/__tests__/step-superpowers.test.tsx +218 -0
  141. package/src/__tests__/story-sections.test.tsx +24 -0
  142. package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
  143. package/src/__tests__/superpower-content.test.tsx +44 -0
  144. package/src/__tests__/suspension-banner.test.tsx +140 -0
  145. package/src/__tests__/tenant-context.test.tsx +146 -0
  146. package/src/__tests__/tenant-keys-api.test.ts +114 -0
  147. package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
  148. package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
  149. package/src/__tests__/terminal-sequence.test.tsx +28 -0
  150. package/src/__tests__/transaction-history.test.tsx +325 -0
  151. package/src/__tests__/trpc-types.test.ts +102 -0
  152. package/src/__tests__/use-capability-meta.test.ts +161 -0
  153. package/src/__tests__/use-chat.test.ts +616 -0
  154. package/src/__tests__/use-has-org.test.ts +44 -0
  155. package/src/__tests__/use-image-status.test.ts +77 -0
  156. package/src/__tests__/use-pagination-params.test.ts +88 -0
  157. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
  158. package/src/__tests__/use-webmcp.test.ts +119 -0
  159. package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
  160. package/src/__tests__/validate-redirect-url.test.ts +61 -0
  161. package/src/__tests__/verify-page.test.tsx +140 -0
  162. package/src/__tests__/verify-redirect.test.tsx +41 -0
  163. package/src/__tests__/verify-result-banner.test.tsx +66 -0
  164. package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
  165. package/src/__tests__/webmcp-hook.test.tsx +72 -0
  166. package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
  167. package/src/__tests__/webmcp-register.test.ts +103 -0
  168. package/src/__tests__/webmcp-set-provider.test.ts +47 -0
  169. package/src/__tests__/webmcp-tools.test.ts +348 -0
  170. package/src/app/(auth)/error.tsx +72 -0
  171. package/src/app/(auth)/forgot-password/page.tsx +137 -0
  172. package/src/app/(auth)/layout.tsx +14 -0
  173. package/src/app/(auth)/loading.tsx +26 -0
  174. package/src/app/(auth)/login/page.tsx +188 -0
  175. package/src/app/(auth)/reset-password/page.tsx +169 -0
  176. package/src/app/(auth)/signup/page.tsx +309 -0
  177. package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
  178. package/src/app/(dashboard)/billing/error.tsx +72 -0
  179. package/src/app/(dashboard)/billing/layout.tsx +73 -0
  180. package/src/app/(dashboard)/billing/loading.tsx +41 -0
  181. package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
  182. package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
  183. package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
  184. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
  185. package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
  186. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
  187. package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
  188. package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
  189. package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
  190. package/src/app/(dashboard)/changesets/error.tsx +72 -0
  191. package/src/app/(dashboard)/changesets/page.tsx +10 -0
  192. package/src/app/(dashboard)/chat/page.tsx +74 -0
  193. package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
  194. package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
  195. package/src/app/(dashboard)/dashboard/page.tsx +13 -0
  196. package/src/app/(dashboard)/error.tsx +72 -0
  197. package/src/app/(dashboard)/layout.tsx +113 -0
  198. package/src/app/(dashboard)/loading.tsx +27 -0
  199. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
  200. package/src/app/(dashboard)/marketplace/error.tsx +72 -0
  201. package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
  202. package/src/app/(dashboard)/marketplace/page.tsx +268 -0
  203. package/src/app/(dashboard)/not-found.tsx +46 -0
  204. package/src/app/(dashboard)/onboarding/page.tsx +267 -0
  205. package/src/app/(dashboard)/settings/account/page.tsx +132 -0
  206. package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
  207. package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
  208. package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
  209. package/src/app/(dashboard)/settings/error.tsx +72 -0
  210. package/src/app/(dashboard)/settings/layout.tsx +114 -0
  211. package/src/app/(dashboard)/settings/loading.tsx +31 -0
  212. package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
  213. package/src/app/(dashboard)/settings/org/page.tsx +617 -0
  214. package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
  215. package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
  216. package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
  217. package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
  218. package/src/app/admin/accounting/loading.tsx +32 -0
  219. package/src/app/admin/accounting/page.tsx +5 -0
  220. package/src/app/admin/affiliates/loading.tsx +32 -0
  221. package/src/app/admin/affiliates/page.tsx +5 -0
  222. package/src/app/admin/audit/loading.tsx +32 -0
  223. package/src/app/admin/audit/page.tsx +5 -0
  224. package/src/app/admin/billing-health/loading.tsx +17 -0
  225. package/src/app/admin/billing-health/page.tsx +10 -0
  226. package/src/app/admin/compliance/page.tsx +5 -0
  227. package/src/app/admin/error.tsx +72 -0
  228. package/src/app/admin/gpu/loading.tsx +38 -0
  229. package/src/app/admin/gpu/page.tsx +5 -0
  230. package/src/app/admin/incidents/page.tsx +10 -0
  231. package/src/app/admin/inference/loading.tsx +32 -0
  232. package/src/app/admin/inference/page.tsx +5 -0
  233. package/src/app/admin/layout.tsx +44 -0
  234. package/src/app/admin/loading.tsx +32 -0
  235. package/src/app/admin/marketplace/loading.tsx +32 -0
  236. package/src/app/admin/marketplace/page.tsx +5 -0
  237. package/src/app/admin/migrations/loading.tsx +22 -0
  238. package/src/app/admin/migrations/page.tsx +5 -0
  239. package/src/app/admin/onboarding/loading.tsx +18 -0
  240. package/src/app/admin/onboarding/page.tsx +5 -0
  241. package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
  242. package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
  243. package/src/app/admin/promotions/[id]/loading.tsx +15 -0
  244. package/src/app/admin/promotions/[id]/page.tsx +311 -0
  245. package/src/app/admin/promotions/loading.tsx +21 -0
  246. package/src/app/admin/promotions/new/loading.tsx +16 -0
  247. package/src/app/admin/promotions/new/page.tsx +12 -0
  248. package/src/app/admin/promotions/page.tsx +266 -0
  249. package/src/app/admin/rate-overrides/loading.tsx +17 -0
  250. package/src/app/admin/rate-overrides/page.tsx +290 -0
  251. package/src/app/admin/roles/loading.tsx +27 -0
  252. package/src/app/admin/roles/page.tsx +5 -0
  253. package/src/app/admin/tenants/loading.tsx +32 -0
  254. package/src/app/admin/tenants/page.tsx +5 -0
  255. package/src/app/apple-icon.tsx +32 -0
  256. package/src/app/auth/callback/[provider]/page.tsx +104 -0
  257. package/src/app/auth/verify/page.tsx +224 -0
  258. package/src/app/channels/error.tsx +72 -0
  259. package/src/app/channels/loading.tsx +29 -0
  260. package/src/app/channels/page.tsx +262 -0
  261. package/src/app/channels/setup/[plugin]/page.tsx +136 -0
  262. package/src/app/error.tsx +72 -0
  263. package/src/app/favicon.ico +0 -0
  264. package/src/app/fleet/error.tsx +72 -0
  265. package/src/app/fleet/health/page.tsx +9 -0
  266. package/src/app/fleet/layout.tsx +14 -0
  267. package/src/app/fleet/loading.tsx +33 -0
  268. package/src/app/fleet/page.tsx +5 -0
  269. package/src/app/global-error.tsx +96 -0
  270. package/src/app/globals.css +251 -0
  271. package/src/app/icon.svg +4 -0
  272. package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
  273. package/src/app/instances/[id]/page.tsx +10 -0
  274. package/src/app/instances/error.tsx +72 -0
  275. package/src/app/instances/instance-list-client.tsx +540 -0
  276. package/src/app/instances/loading.tsx +33 -0
  277. package/src/app/instances/new/create-instance-client.tsx +377 -0
  278. package/src/app/instances/new/page.tsx +9 -0
  279. package/src/app/instances/page.tsx +9 -0
  280. package/src/app/layout.tsx +83 -0
  281. package/src/app/not-found.tsx +38 -0
  282. package/src/app/og/route.tsx +50 -0
  283. package/src/app/page.tsx +39 -0
  284. package/src/app/plugins/error.tsx +72 -0
  285. package/src/app/plugins/layout.tsx +14 -0
  286. package/src/app/plugins/loading.tsx +30 -0
  287. package/src/app/plugins/page.tsx +555 -0
  288. package/src/app/pricing/error.tsx +72 -0
  289. package/src/app/pricing/loading.tsx +25 -0
  290. package/src/app/pricing/page.tsx +20 -0
  291. package/src/app/privacy/page.tsx +406 -0
  292. package/src/app/robots.ts +9 -0
  293. package/src/app/sitemap.ts +11 -0
  294. package/src/app/status/error.tsx +72 -0
  295. package/src/app/status/loading.tsx +21 -0
  296. package/src/app/status/page.tsx +20 -0
  297. package/src/app/terms/page.tsx +414 -0
  298. package/src/components/account-switcher.tsx +82 -0
  299. package/src/components/admin/accounting-dashboard.tsx +190 -0
  300. package/src/components/admin/admin-guard.tsx +36 -0
  301. package/src/components/admin/admin-nav.tsx +71 -0
  302. package/src/components/admin/affiliate-dashboard.tsx +564 -0
  303. package/src/components/admin/audit-log-table.tsx +336 -0
  304. package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
  305. package/src/components/admin/billing-health-dashboard.tsx +416 -0
  306. package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
  307. package/src/components/admin/bulk-actions-bar.tsx +80 -0
  308. package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
  309. package/src/components/admin/bulk-export-dialog.tsx +189 -0
  310. package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
  311. package/src/components/admin/bulk-grant-dialog.tsx +147 -0
  312. package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
  313. package/src/components/admin/bulk-preview-dialog.tsx +106 -0
  314. package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
  315. package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
  316. package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
  317. package/src/components/admin/bulk-select-all-banner.tsx +44 -0
  318. package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
  319. package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
  320. package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
  321. package/src/components/admin/bulk-undo-toast.tsx +121 -0
  322. package/src/components/admin/compliance-dashboard.tsx +1341 -0
  323. package/src/components/admin/gpu-dashboard.tsx +552 -0
  324. package/src/components/admin/grant-credits-dialog.tsx +121 -0
  325. package/src/components/admin/incident-dashboard.test.tsx +44 -0
  326. package/src/components/admin/incident-dashboard.tsx +717 -0
  327. package/src/components/admin/inference-dashboard.tsx +415 -0
  328. package/src/components/admin/marketplace-admin.tsx +765 -0
  329. package/src/components/admin/migrations-dashboard.tsx +404 -0
  330. package/src/components/admin/onboarding-dashboard.tsx +422 -0
  331. package/src/components/admin/promotions/promotion-form.tsx +440 -0
  332. package/src/components/admin/roles-dashboard.tsx +278 -0
  333. package/src/components/admin/suspend-dialog.tsx +98 -0
  334. package/src/components/admin/tenant-notes-panel.tsx +134 -0
  335. package/src/components/admin/tenant-row-actions.tsx +78 -0
  336. package/src/components/admin/tenant-table.tsx +339 -0
  337. package/src/components/auth/auth-error.tsx +22 -0
  338. package/src/components/auth/auth-redirect.tsx +18 -0
  339. package/src/components/auth/auth-shell.tsx +25 -0
  340. package/src/components/auth/email-verification-banner.tsx +25 -0
  341. package/src/components/auth/email-verification-result-banner.tsx +70 -0
  342. package/src/components/auth/resend-verification-button.tsx +94 -0
  343. package/src/components/auth/wopr-wordmark.tsx +19 -0
  344. package/src/components/billing/add-payment-method-dialog.tsx +267 -0
  345. package/src/components/billing/affiliate-dashboard.tsx +300 -0
  346. package/src/components/billing/auto-topup-card.tsx +432 -0
  347. package/src/components/billing/buy-credits-panel.tsx +180 -0
  348. package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
  349. package/src/components/billing/byok-callout.tsx +87 -0
  350. package/src/components/billing/coupon-input.tsx +86 -0
  351. package/src/components/billing/credit-balance.tsx +79 -0
  352. package/src/components/billing/degraded-state-banner.tsx +95 -0
  353. package/src/components/billing/dividend-banner.tsx +97 -0
  354. package/src/components/billing/dividend-eligibility.tsx +86 -0
  355. package/src/components/billing/dividend-pool-stats.tsx +86 -0
  356. package/src/components/billing/first-dividend-dialog.tsx +109 -0
  357. package/src/components/billing/low-balance-banner.tsx +50 -0
  358. package/src/components/billing/org-billing-page.tsx +360 -0
  359. package/src/components/billing/suspension-banner.tsx +53 -0
  360. package/src/components/billing/transaction-history.tsx +239 -0
  361. package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
  362. package/src/components/bot-settings/backups-tab.tsx +377 -0
  363. package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
  364. package/src/components/bot-settings/resources-tab.tsx +203 -0
  365. package/src/components/bot-settings/storage-tab.tsx +248 -0
  366. package/src/components/bot-settings/vps-info-panel.tsx +132 -0
  367. package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
  368. package/src/components/capability/CapabilityResolver.tsx +113 -0
  369. package/src/components/channel-wizard/field-interactive.tsx +48 -0
  370. package/src/components/channel-wizard/field-oauth.tsx +181 -0
  371. package/src/components/channel-wizard/field-paste.tsx +47 -0
  372. package/src/components/channel-wizard/field-qr.tsx +302 -0
  373. package/src/components/channel-wizard/index.ts +6 -0
  374. package/src/components/channel-wizard/step-renderer.tsx +103 -0
  375. package/src/components/channel-wizard/wizard.tsx +200 -0
  376. package/src/components/chat/ambient-dot.tsx +32 -0
  377. package/src/components/chat/chat-input.tsx +56 -0
  378. package/src/components/chat/chat-message.tsx +36 -0
  379. package/src/components/chat/chat-panel.tsx +138 -0
  380. package/src/components/chat/chat-widget.tsx +41 -0
  381. package/src/components/chat/index.ts +5 -0
  382. package/src/components/dashboard/command-center.tsx +614 -0
  383. package/src/components/instances/friends-tab.test.tsx +265 -0
  384. package/src/components/instances/friends-tab.tsx +721 -0
  385. package/src/components/landing/hero.tsx +53 -0
  386. package/src/components/landing/landing-nav.tsx +21 -0
  387. package/src/components/landing/landing-page.tsx +71 -0
  388. package/src/components/landing/portfolio-chart.tsx +349 -0
  389. package/src/components/landing/story-sections.tsx +50 -0
  390. package/src/components/landing/terminal-lines.ts +99 -0
  391. package/src/components/landing/terminal-sequence.tsx +453 -0
  392. package/src/components/landing/typing-effect.tsx +43 -0
  393. package/src/components/marketplace/category-filter.tsx +61 -0
  394. package/src/components/marketplace/empty-state.tsx +61 -0
  395. package/src/components/marketplace/featured-heroes.tsx +84 -0
  396. package/src/components/marketplace/first-visit-hero.tsx +110 -0
  397. package/src/components/marketplace/index.ts +9 -0
  398. package/src/components/marketplace/install-wizard.tsx +782 -0
  399. package/src/components/marketplace/marketplace-tabs.tsx +54 -0
  400. package/src/components/marketplace/plugin-card.tsx +129 -0
  401. package/src/components/marketplace/superpower-card.tsx +104 -0
  402. package/src/components/marketplace/superpower-content.tsx +117 -0
  403. package/src/components/marketplace/terminal-search.tsx +67 -0
  404. package/src/components/oauth-buttons.tsx +75 -0
  405. package/src/components/observability/fleet-health.tsx +370 -0
  406. package/src/components/observability/health-overview.tsx +246 -0
  407. package/src/components/observability/logs-viewer.tsx +215 -0
  408. package/src/components/observability/metrics-dashboard.tsx +288 -0
  409. package/src/components/onboarding/fallback-setup.tsx +137 -0
  410. package/src/components/onboarding/index.ts +3 -0
  411. package/src/components/onboarding/setup-checklist.tsx +333 -0
  412. package/src/components/onboarding/step-superpowers.tsx +122 -0
  413. package/src/components/plugin-setup/index.ts +1 -0
  414. package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
  415. package/src/components/pricing/dividend-calculator.tsx +47 -0
  416. package/src/components/pricing/dividend-stats.tsx +117 -0
  417. package/src/components/pricing/pricing-page.tsx +229 -0
  418. package/src/components/settings/create-org-wizard.tsx +225 -0
  419. package/src/components/sidebar.tsx +202 -0
  420. package/src/components/status/status-page.tsx +209 -0
  421. package/src/components/status-badge.tsx +28 -0
  422. package/src/components/theme-provider.tsx +8 -0
  423. package/src/components/ui/alert-dialog.tsx +141 -0
  424. package/src/components/ui/badge.tsx +47 -0
  425. package/src/components/ui/banner.tsx +36 -0
  426. package/src/components/ui/button.tsx +64 -0
  427. package/src/components/ui/card.tsx +75 -0
  428. package/src/components/ui/checkbox.tsx +52 -0
  429. package/src/components/ui/collapsible.tsx +31 -0
  430. package/src/components/ui/credit-detailed.tsx +33 -0
  431. package/src/components/ui/dialog.tsx +143 -0
  432. package/src/components/ui/dropdown-menu.tsx +228 -0
  433. package/src/components/ui/form.tsx +151 -0
  434. package/src/components/ui/input.tsx +21 -0
  435. package/src/components/ui/label.tsx +21 -0
  436. package/src/components/ui/popover.tsx +74 -0
  437. package/src/components/ui/progress.tsx +28 -0
  438. package/src/components/ui/radio-group.tsx +45 -0
  439. package/src/components/ui/select.tsx +175 -0
  440. package/src/components/ui/separator.tsx +28 -0
  441. package/src/components/ui/sheet.tsx +125 -0
  442. package/src/components/ui/skeleton.tsx +15 -0
  443. package/src/components/ui/switch.tsx +35 -0
  444. package/src/components/ui/table.tsx +92 -0
  445. package/src/components/ui/tabs.tsx +81 -0
  446. package/src/components/ui/textarea.tsx +18 -0
  447. package/src/components/ui/tooltip.tsx +44 -0
  448. package/src/config/provider-docs.ts +17 -0
  449. package/src/hooks/__tests__/use-async.test.ts +127 -0
  450. package/src/hooks/__tests__/use-count-up.test.ts +129 -0
  451. package/src/hooks/__tests__/use-debounce.test.ts +105 -0
  452. package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
  453. package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
  454. package/src/hooks/__tests__/use-mobile.test.ts +86 -0
  455. package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
  456. package/src/hooks/use-async.ts +54 -0
  457. package/src/hooks/use-capability-meta.ts +99 -0
  458. package/src/hooks/use-count-up.ts +23 -0
  459. package/src/hooks/use-debounce.ts +12 -0
  460. package/src/hooks/use-fleet-sse.ts +47 -0
  461. package/src/hooks/use-has-org.ts +18 -0
  462. package/src/hooks/use-image-status.ts +36 -0
  463. package/src/hooks/use-local-storage.ts +36 -0
  464. package/src/hooks/use-mobile.ts +17 -0
  465. package/src/hooks/use-page-context.ts +24 -0
  466. package/src/hooks/use-pagination-params.ts +30 -0
  467. package/src/hooks/use-plugin-registry.ts +247 -0
  468. package/src/hooks/use-plugin-setup-chat.ts +211 -0
  469. package/src/hooks/use-save-queue.ts +54 -0
  470. package/src/hooks/use-webmcp.ts +40 -0
  471. package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
  472. package/src/lib/__tests__/admin-api.test.ts +487 -0
  473. package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
  474. package/src/lib/__tests__/api-fetch.test.ts +196 -0
  475. package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
  476. package/src/lib/__tests__/org-api.test.ts +281 -0
  477. package/src/lib/__tests__/org-billing-api.test.ts +242 -0
  478. package/src/lib/__tests__/pricing-data.test.ts +32 -0
  479. package/src/lib/__tests__/settings-api.test.ts +272 -0
  480. package/src/lib/admin-affiliate-api.ts +51 -0
  481. package/src/lib/admin-api.ts +325 -0
  482. package/src/lib/admin-compliance-api.ts +127 -0
  483. package/src/lib/admin-gpu-api.ts +82 -0
  484. package/src/lib/admin-incident-api.ts +121 -0
  485. package/src/lib/admin-inference-api.ts +47 -0
  486. package/src/lib/admin-marketplace-api.ts +97 -0
  487. package/src/lib/api-config.test.ts +111 -0
  488. package/src/lib/api-config.ts +65 -0
  489. package/src/lib/api-errors.test.ts +43 -0
  490. package/src/lib/api.ts +2011 -0
  491. package/src/lib/auth-client.ts +11 -0
  492. package/src/lib/bot-settings-data.ts +342 -0
  493. package/src/lib/brand-config.ts +145 -0
  494. package/src/lib/brand.ts +669 -0
  495. package/src/lib/changeset-api.ts +29 -0
  496. package/src/lib/changeset-types.ts +56 -0
  497. package/src/lib/channel-manifests.ts +50 -0
  498. package/src/lib/chat/chat-context.tsx +70 -0
  499. package/src/lib/chat/chat-store.ts +62 -0
  500. package/src/lib/chat/types.ts +35 -0
  501. package/src/lib/chat/use-chat.ts +255 -0
  502. package/src/lib/cost-comparison-data.test.ts +95 -0
  503. package/src/lib/cost-comparison-data.ts +54 -0
  504. package/src/lib/errors.test.ts +64 -0
  505. package/src/lib/errors.ts +52 -0
  506. package/src/lib/fetch-utils.test.ts +57 -0
  507. package/src/lib/fetch-utils.ts +25 -0
  508. package/src/lib/format-credit.test.ts +66 -0
  509. package/src/lib/format-credit.ts +24 -0
  510. package/src/lib/format.test.ts +62 -0
  511. package/src/lib/format.ts +17 -0
  512. package/src/lib/logger.ts +28 -0
  513. package/src/lib/marketplace-data.ts +346 -0
  514. package/src/lib/oauth-errors.ts +19 -0
  515. package/src/lib/onboarding-data.ts +1265 -0
  516. package/src/lib/onboarding-store.ts +233 -0
  517. package/src/lib/org-api.ts +74 -0
  518. package/src/lib/org-billing-api.ts +81 -0
  519. package/src/lib/page-prompts.test.ts +32 -0
  520. package/src/lib/page-prompts.ts +23 -0
  521. package/src/lib/plugin/index.ts +32 -0
  522. package/src/lib/plugin/tool-definitions.ts +306 -0
  523. package/src/lib/pricing-data.ts +115 -0
  524. package/src/lib/promotions-types.ts +58 -0
  525. package/src/lib/settings-api.ts +63 -0
  526. package/src/lib/status-colors.ts +38 -0
  527. package/src/lib/tenant-context.tsx +134 -0
  528. package/src/lib/trpc-types.ts +173 -0
  529. package/src/lib/trpc.tsx +86 -0
  530. package/src/lib/utils.test.ts +55 -0
  531. package/src/lib/utils.ts +18 -0
  532. package/src/lib/validate-redirect-url.ts +39 -0
  533. package/src/lib/webmcp/feature-detect.ts +13 -0
  534. package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
  535. package/src/lib/webmcp/register.ts +44 -0
  536. package/src/lib/webmcp/tools.ts +422 -0
  537. package/src/proxy.ts +258 -0
  538. package/src/types/missing-deps.d.ts +160 -0
  539. package/src/types/motion-dom.d.ts +162 -0
  540. package/src/types/vitest-matchers.d.ts +40 -0
  541. package/src/types/web-mcp.d.ts +22 -0
  542. package/tsconfig.json +34 -0
  543. package/vitest.config.ts +26 -0
@@ -0,0 +1,571 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { InstanceDetailClient } from "../app/instances/[id]/instance-detail-client";
5
+
6
+ vi.mock("next/navigation", () => ({
7
+ useSearchParams: () => new URLSearchParams(),
8
+ useRouter: vi.fn().mockReturnValue({ push: vi.fn() }),
9
+ }));
10
+
11
+ vi.mock("@/lib/api", () => ({
12
+ getInstance: vi.fn().mockResolvedValue({
13
+ id: "inst-001",
14
+ name: "test-instance",
15
+ template: "General Assistant",
16
+ status: "running",
17
+ provider: "anthropic",
18
+ channels: ["discord-general"],
19
+ plugins: [
20
+ { id: "p1", name: "memory", version: "1.2.0", enabled: true },
21
+ { id: "p2", name: "web-search", version: "0.9.1", enabled: false },
22
+ ],
23
+ uptime: 86400,
24
+ createdAt: "2026-01-15T10:00:00Z",
25
+ config: { model: "claude-sonnet-4-5-20250514", maxTokens: 4096 },
26
+ channelDetails: [{ id: "ch-0", name: "discord-general", type: "discord", status: "connected" }],
27
+ sessions: [
28
+ {
29
+ id: "sess-001",
30
+ userId: "user-alice",
31
+ messageCount: 42,
32
+ startedAt: "2026-02-12T08:00:00Z",
33
+ lastActivityAt: "2026-02-12T09:30:00Z",
34
+ },
35
+ ],
36
+ resourceUsage: { memoryMb: 256, cpuPercent: 12.5 },
37
+ }),
38
+ controlInstance: vi.fn().mockResolvedValue(undefined),
39
+ updateInstanceConfig: vi.fn().mockResolvedValue(undefined),
40
+ getImageStatus: vi.fn().mockResolvedValue({
41
+ currentDigest: "sha256:aaa",
42
+ latestDigest: "sha256:bbb",
43
+ updateAvailable: true,
44
+ }),
45
+ pullImageUpdate: vi.fn().mockResolvedValue(undefined),
46
+ getInstanceLogs: vi
47
+ .fn()
48
+ .mockResolvedValue([
49
+ { id: "log-1", level: "info", message: "Bot started", timestamp: "2026-02-14T10:00:00Z" },
50
+ ]),
51
+ listSnapshots: vi.fn().mockResolvedValue([
52
+ {
53
+ id: "snap-001",
54
+ instanceId: "inst-001",
55
+ name: "pre-deploy backup",
56
+ type: "on-demand",
57
+ trigger: "manual",
58
+ sizeMb: 128,
59
+ createdAt: "2026-02-20T10:00:00Z",
60
+ expiresAt: null,
61
+ },
62
+ {
63
+ id: "snap-002",
64
+ instanceId: "inst-001",
65
+ name: null,
66
+ type: "nightly",
67
+ trigger: "scheduled",
68
+ sizeMb: 256,
69
+ createdAt: "2026-02-19T03:00:00Z",
70
+ expiresAt: 1740000000,
71
+ },
72
+ ]),
73
+ createSnapshot: vi.fn().mockResolvedValue({
74
+ snapshot: {
75
+ id: "snap-003",
76
+ instanceId: "inst-001",
77
+ name: null,
78
+ type: "on-demand",
79
+ trigger: "manual",
80
+ sizeMb: 64,
81
+ createdAt: "2026-02-24T12:00:00Z",
82
+ expiresAt: null,
83
+ },
84
+ estimatedMonthlyCost: "$0.01/month",
85
+ }),
86
+ restoreSnapshot: vi.fn().mockResolvedValue(undefined),
87
+ deleteSnapshot: vi.fn().mockResolvedValue(undefined),
88
+ getInstanceSecretKeys: vi.fn().mockResolvedValue(["DISCORD_TOKEN", "OPENAI_API_KEY"]),
89
+ updateInstanceSecrets: vi.fn().mockResolvedValue(undefined),
90
+ renameInstance: vi.fn().mockResolvedValue(undefined),
91
+ }));
92
+
93
+ describe("InstanceDetailClient", () => {
94
+ it("renders instance name and status", async () => {
95
+ render(<InstanceDetailClient instanceId="inst-001" />);
96
+
97
+ await waitFor(() => {
98
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
99
+ });
100
+ expect(screen.getByText("Running")).toBeInTheDocument();
101
+ });
102
+
103
+ it("renders all tab triggers", async () => {
104
+ render(<InstanceDetailClient instanceId="inst-001" />);
105
+
106
+ await waitFor(() => {
107
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
108
+ });
109
+
110
+ expect(screen.getByRole("tab", { name: "Overview" })).toBeInTheDocument();
111
+ expect(screen.getByRole("tab", { name: "Plugins" })).toBeInTheDocument();
112
+ expect(screen.getByRole("tab", { name: "Channels" })).toBeInTheDocument();
113
+ expect(screen.getByRole("tab", { name: "Sessions" })).toBeInTheDocument();
114
+ expect(screen.getByRole("tab", { name: "Config" })).toBeInTheDocument();
115
+ expect(screen.getByRole("tab", { name: "Logs" })).toBeInTheDocument();
116
+ expect(screen.getByRole("tab", { name: "Snapshots" })).toBeInTheDocument();
117
+ });
118
+
119
+ it("shows overview metrics by default", async () => {
120
+ render(<InstanceDetailClient instanceId="inst-001" />);
121
+
122
+ await waitFor(() => {
123
+ expect(screen.getByText("256 MB")).toBeInTheDocument();
124
+ });
125
+ expect(screen.getByText("12.5%")).toBeInTheDocument();
126
+ expect(screen.getByText("running")).toBeInTheDocument();
127
+ });
128
+
129
+ it("shows action buttons for running instance", async () => {
130
+ render(<InstanceDetailClient instanceId="inst-001" />);
131
+
132
+ await waitFor(() => {
133
+ expect(screen.getByText("Stop")).toBeInTheDocument();
134
+ });
135
+ expect(screen.getByText("Restart")).toBeInTheDocument();
136
+ expect(screen.getByText("Destroy")).toBeInTheDocument();
137
+ });
138
+
139
+ it("tabs can be clicked and become selected", async () => {
140
+ const user = userEvent.setup();
141
+ render(<InstanceDetailClient instanceId="inst-001" />);
142
+
143
+ await waitFor(() => {
144
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
145
+ });
146
+
147
+ // Verify all tabs are rendered
148
+ const pluginsTab = screen.getByRole("tab", { name: "Plugins" });
149
+ const channelsTab = screen.getByRole("tab", { name: "Channels" });
150
+ const sessionsTab = screen.getByRole("tab", { name: "Sessions" });
151
+ const configTab = screen.getByRole("tab", { name: "Config" });
152
+ const logsTab = screen.getByRole("tab", { name: "Logs" });
153
+
154
+ expect(pluginsTab).toBeInTheDocument();
155
+ expect(channelsTab).toBeInTheDocument();
156
+ expect(sessionsTab).toBeInTheDocument();
157
+ expect(configTab).toBeInTheDocument();
158
+ expect(logsTab).toBeInTheDocument();
159
+
160
+ // Click Plugins tab and verify it becomes selected
161
+ await user.click(pluginsTab);
162
+ expect(pluginsTab).toHaveAttribute("aria-selected", "true");
163
+
164
+ // Click Channels tab and verify it becomes selected
165
+ await user.click(channelsTab);
166
+ expect(channelsTab).toHaveAttribute("aria-selected", "true");
167
+
168
+ // Click Config tab and verify it becomes selected
169
+ await user.click(configTab);
170
+ expect(configTab).toHaveAttribute("aria-selected", "true");
171
+
172
+ // Click Sessions tab and verify it becomes selected
173
+ await user.click(sessionsTab);
174
+ expect(sessionsTab).toHaveAttribute("aria-selected", "true");
175
+
176
+ // Click Logs tab and verify it becomes selected
177
+ await user.click(logsTab);
178
+ expect(logsTab).toHaveAttribute("aria-selected", "true");
179
+ expect(screen.getByText("Bot started")).toBeInTheDocument();
180
+ });
181
+
182
+ it("clicking Save Config calls updateInstanceConfig with parsed JSON", async () => {
183
+ const user = userEvent.setup();
184
+ render(<InstanceDetailClient instanceId="inst-001" />);
185
+
186
+ await waitFor(() => {
187
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
188
+ });
189
+
190
+ const configTab = screen.getByRole("tab", { name: "Config" });
191
+ await user.click(configTab);
192
+
193
+ const saveBtn = screen.getByText("Save Config");
194
+ await user.click(saveBtn);
195
+
196
+ const { updateInstanceConfig } = await import("@/lib/api");
197
+ expect(updateInstanceConfig).toHaveBeenCalledWith("inst-001", {
198
+ model: "claude-sonnet-4-5-20250514",
199
+ maxTokens: "4096",
200
+ });
201
+ });
202
+
203
+ it("clicking Stop button calls controlInstance with correct args", async () => {
204
+ const user = userEvent.setup();
205
+ const { controlInstance } = await import("@/lib/api");
206
+ render(<InstanceDetailClient instanceId="inst-001" />);
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByText("Stop")).toBeInTheDocument();
210
+ });
211
+
212
+ const stopBtn = screen.getByText("Stop");
213
+ await user.click(stopBtn);
214
+
215
+ expect(controlInstance).toHaveBeenCalledWith("inst-001", "stop");
216
+ });
217
+
218
+ it("shows snapshot list when Snapshots tab is clicked", async () => {
219
+ const user = userEvent.setup();
220
+ render(<InstanceDetailClient instanceId="inst-001" />);
221
+
222
+ await waitFor(() => {
223
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
224
+ });
225
+
226
+ const snapshotsTab = screen.getByRole("tab", { name: "Snapshots" });
227
+ await user.click(snapshotsTab);
228
+
229
+ await waitFor(() => {
230
+ expect(screen.getByText("pre-deploy backup")).toBeInTheDocument();
231
+ });
232
+ expect(screen.getByText("128 MB")).toBeInTheDocument();
233
+ expect(screen.getByText("on-demand")).toBeInTheDocument();
234
+ expect(screen.getByText("nightly")).toBeInTheDocument();
235
+ });
236
+
237
+ it("shows Create Snapshot button in Snapshots tab", async () => {
238
+ const user = userEvent.setup();
239
+ render(<InstanceDetailClient instanceId="inst-001" />);
240
+
241
+ await waitFor(() => {
242
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
243
+ });
244
+
245
+ await user.click(screen.getByRole("tab", { name: "Snapshots" }));
246
+
247
+ await waitFor(() => {
248
+ expect(screen.getByRole("button", { name: "Create Snapshot" })).toBeInTheDocument();
249
+ });
250
+ });
251
+
252
+ it("calls createSnapshot when Create Snapshot is clicked", async () => {
253
+ const user = userEvent.setup();
254
+ const { createSnapshot } = await import("@/lib/api");
255
+ render(<InstanceDetailClient instanceId="inst-001" />);
256
+
257
+ await waitFor(() => {
258
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
259
+ });
260
+
261
+ await user.click(screen.getByRole("tab", { name: "Snapshots" }));
262
+ await waitFor(() => {
263
+ expect(screen.getByRole("button", { name: "Create Snapshot" })).toBeInTheDocument();
264
+ });
265
+
266
+ await user.click(screen.getByRole("button", { name: "Create Snapshot" }));
267
+ expect(createSnapshot).toHaveBeenCalledWith("inst-001");
268
+ });
269
+
270
+ it("shows restore confirmation dialog", async () => {
271
+ const user = userEvent.setup();
272
+ render(<InstanceDetailClient instanceId="inst-001" />);
273
+ await waitFor(() => {
274
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
275
+ });
276
+
277
+ await user.click(screen.getByRole("tab", { name: "Snapshots" }));
278
+ await waitFor(() => {
279
+ expect(screen.getByText("pre-deploy backup")).toBeInTheDocument();
280
+ });
281
+
282
+ const restoreButtons = screen.getAllByRole("button", { name: "Restore" });
283
+ await user.click(restoreButtons[0]);
284
+
285
+ await waitFor(() => {
286
+ expect(screen.getByText("This will restart your bot from this snapshot")).toBeInTheDocument();
287
+ });
288
+ });
289
+
290
+ it("shows delete confirmation dialog", async () => {
291
+ const user = userEvent.setup();
292
+ render(<InstanceDetailClient instanceId="inst-001" />);
293
+ await waitFor(() => {
294
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
295
+ });
296
+
297
+ await user.click(screen.getByRole("tab", { name: "Snapshots" }));
298
+ await waitFor(() => {
299
+ expect(screen.getByText("pre-deploy backup")).toBeInTheDocument();
300
+ });
301
+
302
+ const deleteButtons = screen.getAllByRole("button", { name: "Delete" });
303
+ await user.click(deleteButtons[0]);
304
+
305
+ await waitFor(() => {
306
+ expect(screen.getByText(/permanently delete/i)).toBeInTheDocument();
307
+ });
308
+ });
309
+
310
+ it("shows update available badge when image update exists", async () => {
311
+ render(<InstanceDetailClient instanceId="inst-001" />);
312
+
313
+ await waitFor(() => {
314
+ expect(screen.getByText("Update available")).toBeInTheDocument();
315
+ });
316
+ });
317
+
318
+ it("shows Pull Update button when update is available", async () => {
319
+ render(<InstanceDetailClient instanceId="inst-001" />);
320
+
321
+ await waitFor(() => {
322
+ expect(screen.getByText("Pull Update")).toBeInTheDocument();
323
+ });
324
+ });
325
+
326
+ it("calls pullImageUpdate when Pull Update button is clicked and confirmed via AlertDialog", async () => {
327
+ const user = userEvent.setup();
328
+ const { pullImageUpdate } = await import("@/lib/api");
329
+
330
+ render(<InstanceDetailClient instanceId="inst-001" />);
331
+
332
+ await waitFor(() => {
333
+ expect(screen.getByText("Pull Update")).toBeInTheDocument();
334
+ });
335
+
336
+ await user.click(screen.getByText("Pull Update"));
337
+
338
+ await waitFor(() => {
339
+ expect(
340
+ screen.getByText("This will pull the latest image and restart the bot. Continue?"),
341
+ ).toBeInTheDocument();
342
+ });
343
+
344
+ await user.click(screen.getByRole("button", { name: "Continue" }));
345
+ expect(pullImageUpdate).toHaveBeenCalledWith("inst-001");
346
+ });
347
+
348
+ it("does not call pullImageUpdate when user cancels AlertDialog", async () => {
349
+ const user = userEvent.setup();
350
+ const { pullImageUpdate } = await import("@/lib/api");
351
+ vi.mocked(pullImageUpdate).mockClear();
352
+
353
+ render(<InstanceDetailClient instanceId="inst-001" />);
354
+
355
+ await waitFor(() => {
356
+ expect(screen.getByText("Pull Update")).toBeInTheDocument();
357
+ });
358
+
359
+ await user.click(screen.getByText("Pull Update"));
360
+
361
+ await waitFor(() => {
362
+ expect(
363
+ screen.getByText("This will pull the latest image and restart the bot. Continue?"),
364
+ ).toBeInTheDocument();
365
+ });
366
+
367
+ await user.click(screen.getByRole("button", { name: "Cancel" }));
368
+ expect(pullImageUpdate).not.toHaveBeenCalled();
369
+ });
370
+
371
+ it("shows Secrets section in Config tab with existing secret keys", async () => {
372
+ const user = userEvent.setup();
373
+ render(<InstanceDetailClient instanceId="inst-001" />);
374
+
375
+ await waitFor(() => {
376
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
377
+ });
378
+
379
+ await user.click(screen.getByRole("tab", { name: "Config" }));
380
+
381
+ await waitFor(() => {
382
+ expect(screen.getByText("Secrets")).toBeInTheDocument();
383
+ });
384
+ expect(screen.getByText("DISCORD_TOKEN")).toBeInTheDocument();
385
+ expect(screen.getByText("OPENAI_API_KEY")).toBeInTheDocument();
386
+ });
387
+
388
+ it("calls updateInstanceSecrets when saving a secret value", async () => {
389
+ const user = userEvent.setup();
390
+ const { updateInstanceSecrets } = await import("@/lib/api");
391
+
392
+ render(<InstanceDetailClient instanceId="inst-001" />);
393
+ await waitFor(() => {
394
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
395
+ });
396
+
397
+ await user.click(screen.getByRole("tab", { name: "Config" }));
398
+ await waitFor(() => {
399
+ expect(screen.getByText("Secrets")).toBeInTheDocument();
400
+ });
401
+
402
+ const inputs = screen.getAllByPlaceholderText("Enter new value...");
403
+ await user.type(inputs[0], "new-secret-value");
404
+
405
+ await user.click(screen.getByRole("button", { name: "Save Secrets" }));
406
+
407
+ expect(updateInstanceSecrets).toHaveBeenCalledWith("inst-001", {
408
+ DISCORD_TOKEN: "new-secret-value",
409
+ });
410
+ });
411
+
412
+ it("allows adding a new secret key", async () => {
413
+ const user = userEvent.setup();
414
+ render(<InstanceDetailClient instanceId="inst-001" />);
415
+ await waitFor(() => {
416
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
417
+ });
418
+
419
+ await user.click(screen.getByRole("tab", { name: "Config" }));
420
+ await waitFor(() => {
421
+ expect(screen.getByText("Secrets")).toBeInTheDocument();
422
+ });
423
+
424
+ await user.click(screen.getByRole("button", { name: "Add Secret" }));
425
+
426
+ expect(screen.getByPlaceholderText("SECRET_KEY_NAME")).toBeInTheDocument();
427
+ });
428
+
429
+ it("clicking Rename opens inline edit, submitting calls renameInstance", async () => {
430
+ const user = userEvent.setup();
431
+ render(<InstanceDetailClient instanceId="inst-001" />);
432
+
433
+ await waitFor(() => {
434
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
435
+ });
436
+
437
+ const renameBtn = screen.getByRole("button", { name: /rename/i });
438
+ await user.click(renameBtn);
439
+
440
+ const input = screen.getByDisplayValue("test-instance");
441
+ expect(input).toBeInTheDocument();
442
+
443
+ await user.clear(input);
444
+ await user.type(input, "renamed-bot");
445
+
446
+ const confirmBtn = screen.getByRole("button", { name: /confirm rename/i });
447
+ await user.click(confirmBtn);
448
+
449
+ const { renameInstance } = await import("@/lib/api");
450
+ expect(renameInstance).toHaveBeenCalledWith("inst-001", "renamed-bot");
451
+ });
452
+
453
+ it("shows invalid JSON indicator in real-time as user types", async () => {
454
+ const user = userEvent.setup();
455
+ render(<InstanceDetailClient instanceId="inst-001" />);
456
+
457
+ await waitFor(() => {
458
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
459
+ });
460
+
461
+ const configTab = screen.getByRole("tab", { name: "Config" });
462
+ await user.click(configTab);
463
+
464
+ const editor = await screen.findByRole("textbox", { name: /instance configuration/i });
465
+
466
+ await user.clear(editor);
467
+ await user.type(editor, "{{} not valid json");
468
+
469
+ await waitFor(() => {
470
+ expect(screen.getByText("Invalid JSON")).toBeInTheDocument();
471
+ });
472
+ });
473
+
474
+ it("clears invalid JSON indicator when user fixes the JSON", async () => {
475
+ const user = userEvent.setup();
476
+ render(<InstanceDetailClient instanceId="inst-001" />);
477
+
478
+ await waitFor(() => {
479
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
480
+ });
481
+
482
+ const configTab = screen.getByRole("tab", { name: "Config" });
483
+ await user.click(configTab);
484
+
485
+ const editor = await screen.findByRole("textbox", { name: /instance configuration/i });
486
+
487
+ await user.clear(editor);
488
+ await user.type(editor, "{{} broken");
489
+
490
+ await waitFor(() => {
491
+ expect(screen.getByText("Invalid JSON")).toBeInTheDocument();
492
+ });
493
+
494
+ await user.clear(editor);
495
+ await user.type(editor, '{{"model": "claude"}');
496
+
497
+ await waitFor(() => {
498
+ expect(screen.queryByText("Invalid JSON")).not.toBeInTheDocument();
499
+ });
500
+ });
501
+
502
+ it("disables save button when JSON is invalid", async () => {
503
+ const user = userEvent.setup();
504
+ render(<InstanceDetailClient instanceId="inst-001" />);
505
+
506
+ await waitFor(() => {
507
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
508
+ });
509
+
510
+ const configTab = screen.getByRole("tab", { name: "Config" });
511
+ await user.click(configTab);
512
+
513
+ const editor = await screen.findByRole("textbox", { name: /instance configuration/i });
514
+
515
+ await user.clear(editor);
516
+ await user.type(editor, "not json");
517
+
518
+ await waitFor(() => {
519
+ expect(screen.getByRole("button", { name: /save config/i })).toBeDisabled();
520
+ });
521
+ });
522
+
523
+ it("shows invalid JSON indicator after a successful save when user edits to invalid JSON", async () => {
524
+ const user = userEvent.setup();
525
+ render(<InstanceDetailClient instanceId="inst-001" />);
526
+
527
+ await waitFor(() => {
528
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
529
+ });
530
+
531
+ const configTab = screen.getByRole("tab", { name: "Config" });
532
+ await user.click(configTab);
533
+
534
+ // Save the config successfully first
535
+ const saveBtn = screen.getByText("Save Config");
536
+ await user.click(saveBtn);
537
+
538
+ await waitFor(() => {
539
+ expect(screen.getByText("Config saved")).toBeInTheDocument();
540
+ });
541
+
542
+ // Now edit to invalid JSON — the indicator must appear immediately
543
+ const editor = await screen.findByRole("textbox", { name: /instance configuration/i });
544
+ await user.clear(editor);
545
+ await user.type(editor, "not valid json {{");
546
+
547
+ await waitFor(() => {
548
+ expect(screen.getByText("Invalid JSON")).toBeInTheDocument();
549
+ });
550
+ });
551
+
552
+ it("rename cancel restores original name", async () => {
553
+ const user = userEvent.setup();
554
+ render(<InstanceDetailClient instanceId="inst-001" />);
555
+
556
+ await waitFor(() => {
557
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
558
+ });
559
+
560
+ const renameBtn = screen.getByRole("button", { name: /rename/i });
561
+ await user.click(renameBtn);
562
+
563
+ const input = screen.getByDisplayValue("test-instance");
564
+ await user.clear(input);
565
+ await user.type(input, "something-else");
566
+
567
+ await user.keyboard("{Escape}");
568
+
569
+ expect(screen.getByText("test-instance")).toBeInTheDocument();
570
+ });
571
+ });