@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,1341 @@
1
+ "use client";
2
+
3
+ import { Download, FileText, Pencil, Search, Shield, Trash2 } from "lucide-react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { toast } from "sonner";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog";
15
+ import { Input } from "@/components/ui/input";
16
+ import { Label } from "@/components/ui/label";
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "@/components/ui/select";
24
+ import { Skeleton } from "@/components/ui/skeleton";
25
+ import { Switch } from "@/components/ui/switch";
26
+ import {
27
+ Table,
28
+ TableBody,
29
+ TableCell,
30
+ TableHead,
31
+ TableHeader,
32
+ TableRow,
33
+ } from "@/components/ui/table";
34
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
35
+ import { Textarea } from "@/components/ui/textarea";
36
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
37
+ import type { DeletionRequest, ExportRequest, RetentionPolicy } from "@/lib/admin-compliance-api";
38
+ import {
39
+ cancelDeletion,
40
+ fetchRetentionPolicies,
41
+ getDeletionRequests,
42
+ getExportRequests,
43
+ triggerDeletion,
44
+ triggerExport,
45
+ updateRetentionPolicy,
46
+ } from "@/lib/admin-compliance-api";
47
+ import type { AuditLogResponse } from "@/lib/api";
48
+ import { fetchAuditLog } from "@/lib/api";
49
+ import { toUserMessage } from "@/lib/errors";
50
+ import { cn } from "@/lib/utils";
51
+
52
+ const PAGE_SIZE = 50;
53
+
54
+ const STATUS_FILTERS = [
55
+ { label: "All Statuses", value: "all" },
56
+ { label: "Pending", value: "pending" },
57
+ { label: "Completed", value: "completed" },
58
+ { label: "Cancelled", value: "cancelled" },
59
+ ] as const;
60
+
61
+ const AUDIT_ACTION_FILTERS = [
62
+ { label: "All Actions", value: "all" },
63
+ { label: "Deletion", value: "compliance.trigger_deletion" },
64
+ { label: "Export", value: "compliance.trigger_export" },
65
+ { label: "Policy Change", value: "compliance.policy_update" },
66
+ ] as const;
67
+
68
+ const DATE_RANGES = [
69
+ { label: "Last 24 hours", value: "1" },
70
+ { label: "Last 7 days", value: "7" },
71
+ { label: "Last 30 days", value: "30" },
72
+ { label: "Last 90 days", value: "90" },
73
+ { label: "All time", value: "all" },
74
+ ] as const;
75
+
76
+ // --- Utilities ---
77
+
78
+ function relativeTime(iso: string): string {
79
+ const rawDiff = Date.now() - new Date(iso).getTime();
80
+ const isFuture = rawDiff < 0;
81
+ const diff = Math.abs(rawDiff);
82
+ const seconds = Math.floor(diff / 1000);
83
+ if (seconds < 60) return "just now";
84
+ const minutes = Math.floor(seconds / 60);
85
+ if (minutes < 60) return isFuture ? `in ${minutes}m` : `${minutes}m ago`;
86
+ const hours = Math.floor(minutes / 60);
87
+ if (hours < 24) return isFuture ? `in ${hours}h` : `${hours}h ago`;
88
+ const days = Math.floor(hours / 24);
89
+ return isFuture ? `in ${days}d` : `${days}d ago`;
90
+ }
91
+
92
+ function isSafeUrl(url: string): boolean {
93
+ return url.startsWith("https://") || (url.startsWith("/") && !url.startsWith("//"));
94
+ }
95
+
96
+ function statusBadgeClasses(status: string): string {
97
+ if (status === "pending") return "bg-amber-500/15 text-amber-400 border border-amber-500/20";
98
+ if (status === "completed") return "bg-terminal/15 text-terminal border border-terminal/20";
99
+ if (status === "in_progress") return "bg-blue-500/15 text-blue-400 border border-blue-500/20";
100
+ if (status === "failed") return "bg-destructive/15 text-red-400 border border-destructive/20";
101
+ return "bg-secondary text-muted-foreground border border-border";
102
+ }
103
+
104
+ function complianceActionBadgeClasses(action: string): string {
105
+ if (action.includes("trigger_deletion") || action.includes("complete_deletion"))
106
+ return "bg-destructive/15 text-red-400 border border-destructive/20";
107
+ if (action.includes("trigger_export") || action.includes("complete_export"))
108
+ return "bg-terminal/15 text-terminal border border-terminal/20";
109
+ if (action.includes("policy_update"))
110
+ return "bg-amber-500/15 text-amber-400 border border-amber-500/20";
111
+ return "bg-secondary text-muted-foreground border border-border";
112
+ }
113
+
114
+ function humanAction(action: string): string {
115
+ return action.replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
116
+ }
117
+
118
+ // --- Skeleton rows ---
119
+
120
+ const SKEL_ROWS = ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8"] as const;
121
+ const SKEL_COLS_4 = ["c1", "c2", "c3", "c4"] as const;
122
+ const SKEL_COLS_6 = ["c1", "c2", "c3", "c4", "c5", "c6"] as const;
123
+
124
+ function SkeletonRows({ cols, rows = 6 }: { cols: number; rows?: number }) {
125
+ const colKeys = cols <= 4 ? SKEL_COLS_4 : SKEL_COLS_6;
126
+ const rowKeys = SKEL_ROWS.slice(0, rows);
127
+ return (
128
+ <>
129
+ {rowKeys.map((rowKey, i) => (
130
+ <TableRow key={rowKey} className="h-10">
131
+ {colKeys.slice(0, cols).map((colKey) => (
132
+ <TableCell key={`${rowKey}-${colKey}`}>
133
+ <Skeleton className="h-4 w-full" style={{ animationDelay: `${i * 50}ms` }} />
134
+ </TableCell>
135
+ ))}
136
+ </TableRow>
137
+ ))}
138
+ </>
139
+ );
140
+ }
141
+
142
+ function EmptyState({ message }: { message: string }) {
143
+ return (
144
+ <TableRow>
145
+ <TableCell colSpan={99} className="h-32 text-center">
146
+ <p className="text-sm text-muted-foreground font-mono">
147
+ &gt; {message}
148
+ <span className="animate-ellipsis" />
149
+ </p>
150
+ </TableCell>
151
+ </TableRow>
152
+ );
153
+ }
154
+
155
+ // --- Trigger Dialog ---
156
+
157
+ function TriggerDialog({
158
+ open,
159
+ onOpenChange,
160
+ title,
161
+ description,
162
+ confirmLabel,
163
+ confirmClassName,
164
+ onConfirm,
165
+ }: {
166
+ open: boolean;
167
+ onOpenChange: (open: boolean) => void;
168
+ title: string;
169
+ description: string;
170
+ confirmLabel: string;
171
+ confirmClassName: string;
172
+ onConfirm: (tenantId: string, reason: string) => Promise<void>;
173
+ }) {
174
+ const [tenantId, setTenantId] = useState("");
175
+ const [reason, setReason] = useState("");
176
+ const [submitting, setSubmitting] = useState(false);
177
+
178
+ const handleOpenChange = (open: boolean) => {
179
+ if (!open) {
180
+ setTenantId("");
181
+ setReason("");
182
+ }
183
+ onOpenChange(open);
184
+ };
185
+
186
+ const handleConfirm = async () => {
187
+ if (!tenantId.trim() || !reason.trim()) return;
188
+ setSubmitting(true);
189
+ try {
190
+ await onConfirm(tenantId.trim(), reason.trim());
191
+ onOpenChange(false);
192
+ setTenantId("");
193
+ setReason("");
194
+ } catch (err) {
195
+ toast.error(toUserMessage(err));
196
+ } finally {
197
+ setSubmitting(false);
198
+ }
199
+ };
200
+
201
+ return (
202
+ <Dialog open={open} onOpenChange={handleOpenChange}>
203
+ <DialogContent>
204
+ <DialogHeader>
205
+ <DialogTitle className="text-base font-semibold uppercase tracking-wider">
206
+ {title}
207
+ </DialogTitle>
208
+ <DialogDescription>{description}</DialogDescription>
209
+ </DialogHeader>
210
+ <div className="space-y-3">
211
+ <Input
212
+ placeholder="Tenant ID"
213
+ value={tenantId}
214
+ onChange={(e) => setTenantId(e.target.value)}
215
+ className="focus:border-terminal focus:ring-terminal/20"
216
+ />
217
+ <Textarea
218
+ placeholder="Reason (required)"
219
+ value={reason}
220
+ onChange={(e) => setReason(e.target.value)}
221
+ className="min-h-[80px] focus:border-terminal focus:ring-terminal/20"
222
+ />
223
+ </div>
224
+ <DialogFooter>
225
+ <Button variant="ghost" onClick={() => handleOpenChange(false)} disabled={submitting}>
226
+ Cancel
227
+ </Button>
228
+ <Button
229
+ variant="outline"
230
+ className={confirmClassName}
231
+ onClick={handleConfirm}
232
+ disabled={!tenantId.trim() || !reason.trim() || submitting}
233
+ >
234
+ {submitting ? "Processing..." : confirmLabel}
235
+ </Button>
236
+ </DialogFooter>
237
+ </DialogContent>
238
+ </Dialog>
239
+ );
240
+ }
241
+
242
+ // --- Pagination ---
243
+
244
+ function Pagination({
245
+ offset,
246
+ total,
247
+ hasMore,
248
+ onNavigate,
249
+ }: {
250
+ offset: number;
251
+ total: number;
252
+ hasMore: boolean;
253
+ onNavigate: (offset: number) => void;
254
+ }) {
255
+ if (total <= PAGE_SIZE) return null;
256
+ return (
257
+ <div className="flex items-center justify-between text-xs text-muted-foreground font-mono">
258
+ <span>
259
+ Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
260
+ </span>
261
+ <div className="flex gap-2">
262
+ <Button variant="ghost" size="sm" disabled={offset === 0} onClick={() => onNavigate(0)}>
263
+ First
264
+ </Button>
265
+ <Button
266
+ variant="ghost"
267
+ size="sm"
268
+ disabled={offset === 0}
269
+ onClick={() => onNavigate(Math.max(0, offset - PAGE_SIZE))}
270
+ >
271
+ Previous
272
+ </Button>
273
+ <Button
274
+ variant="ghost"
275
+ size="sm"
276
+ disabled={!hasMore}
277
+ onClick={() => onNavigate(offset + PAGE_SIZE)}
278
+ >
279
+ Next
280
+ </Button>
281
+ <Button
282
+ variant="ghost"
283
+ size="sm"
284
+ disabled={!hasMore}
285
+ onClick={() => {
286
+ const lastPageOffset = Math.floor((total - 1) / PAGE_SIZE) * PAGE_SIZE;
287
+ onNavigate(lastPageOffset);
288
+ }}
289
+ >
290
+ Last
291
+ </Button>
292
+ </div>
293
+ </div>
294
+ );
295
+ }
296
+
297
+ // ============================================================
298
+ // TAB 1: Deletion Requests
299
+ // ============================================================
300
+
301
+ function DeletionRequestsTab() {
302
+ const [data, setData] = useState<{
303
+ requests: DeletionRequest[];
304
+ total: number;
305
+ } | null>(null);
306
+ const [loading, setLoading] = useState(true);
307
+ const [loadError, setLoadError] = useState<string | null>(null);
308
+ const [offset, setOffset] = useState(0);
309
+ const [statusFilter, setStatusFilter] = useState("all");
310
+ const [search, setSearch] = useState("");
311
+ const [dialogOpen, setDialogOpen] = useState(false);
312
+
313
+ const load = useCallback(
314
+ async (newOffset: number, signal?: AbortSignal) => {
315
+ setLoading(true);
316
+ setLoadError(null);
317
+ try {
318
+ const result = await getDeletionRequests({
319
+ status: statusFilter === "all" ? undefined : statusFilter,
320
+ limit: PAGE_SIZE,
321
+ offset: newOffset,
322
+ });
323
+ if (signal?.aborted) return;
324
+ setData(result);
325
+ setOffset(newOffset);
326
+ } catch (err) {
327
+ if (signal?.aborted) return;
328
+ const message = err instanceof Error ? err.message : "Failed to load";
329
+ if (message.includes("NOT_IMPLEMENTED") || message.includes("not found")) {
330
+ setLoadError("backend_missing");
331
+ } else {
332
+ setLoadError(message);
333
+ }
334
+ } finally {
335
+ if (!signal?.aborted) setLoading(false);
336
+ }
337
+ },
338
+ [statusFilter],
339
+ );
340
+
341
+ useEffect(() => {
342
+ const controller = new AbortController();
343
+ load(0, controller.signal);
344
+ return () => controller.abort();
345
+ }, [load]);
346
+
347
+ const filteredRequests = useMemo(() => {
348
+ if (!data) return [];
349
+ if (!search.trim()) return data.requests;
350
+ const q = search.toLowerCase();
351
+ return data.requests.filter(
352
+ (r) =>
353
+ r.tenantId.toLowerCase().includes(q) ||
354
+ r.requestedBy.toLowerCase().includes(q) ||
355
+ r.status.toLowerCase().includes(q),
356
+ );
357
+ }, [data, search]);
358
+
359
+ const handleTriggerDeletion = async (tenantId: string, reason: string) => {
360
+ await triggerDeletion(tenantId, reason);
361
+ toast.success("Deletion request created");
362
+ load(offset);
363
+ };
364
+
365
+ const handleCancelDeletion = async (requestId: string) => {
366
+ try {
367
+ await cancelDeletion(requestId);
368
+ toast.success("Deletion request cancelled");
369
+ load(offset);
370
+ } catch (err) {
371
+ toast.error(toUserMessage(err));
372
+ }
373
+ };
374
+
375
+ // Backend endpoints not yet available
376
+ if (loadError === "backend_missing") {
377
+ return (
378
+ <div className="space-y-3">
379
+ <div className="flex flex-wrap items-center justify-between gap-2">
380
+ <div className="flex flex-wrap items-center gap-2">
381
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
382
+ <SelectTrigger className="w-[150px]">
383
+ <SelectValue />
384
+ </SelectTrigger>
385
+ <SelectContent>
386
+ {STATUS_FILTERS.map((f) => (
387
+ <SelectItem key={f.value} value={f.value}>
388
+ {f.label}
389
+ </SelectItem>
390
+ ))}
391
+ </SelectContent>
392
+ </Select>
393
+ <div className="relative max-w-sm">
394
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
395
+ <Input
396
+ placeholder="Search requests..."
397
+ value={search}
398
+ onChange={(e) => setSearch(e.target.value)}
399
+ className="pl-9 focus:border-terminal focus:ring-terminal/20"
400
+ disabled
401
+ />
402
+ </div>
403
+ </div>
404
+ <Button
405
+ variant="outline"
406
+ className="border-terminal/30 text-terminal hover:bg-terminal/10"
407
+ onClick={() => setDialogOpen(true)}
408
+ disabled
409
+ >
410
+ <Trash2 className="mr-1.5 size-3.5" />
411
+ Trigger Deletion
412
+ </Button>
413
+ </div>
414
+ <div className="rounded-sm border border-terminal/10 p-8 text-center">
415
+ <p className="text-sm text-muted-foreground font-mono">
416
+ &gt; Backend endpoints for listing deletion requests are pending deployment
417
+ <span className="animate-ellipsis" />
418
+ </p>
419
+ <p className="mt-2 text-xs text-muted-foreground">
420
+ The admin.complianceDeletionRequests procedure is not yet available. This section will
421
+ activate automatically once deployed.
422
+ </p>
423
+ </div>
424
+ <TriggerDialog
425
+ open={dialogOpen}
426
+ onOpenChange={setDialogOpen}
427
+ title="Trigger Data Deletion"
428
+ description="Schedule all personal data for deletion for a specific tenant. This action is audited."
429
+ confirmLabel="Confirm Deletion"
430
+ confirmClassName="border-destructive/30 text-red-400 hover:bg-destructive/10"
431
+ onConfirm={handleTriggerDeletion}
432
+ />
433
+ </div>
434
+ );
435
+ }
436
+
437
+ if (loadError) {
438
+ return (
439
+ <div className="flex h-40 flex-col items-center justify-center gap-3">
440
+ <p className="text-sm text-destructive font-mono">Failed to load deletion requests.</p>
441
+ <Button variant="outline" size="sm" onClick={() => load(offset)}>
442
+ Retry
443
+ </Button>
444
+ </div>
445
+ );
446
+ }
447
+
448
+ return (
449
+ <div className="space-y-3">
450
+ <div className="flex flex-wrap items-center justify-between gap-2">
451
+ <div className="flex flex-wrap items-center gap-2">
452
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
453
+ <SelectTrigger className="w-[150px]">
454
+ <SelectValue />
455
+ </SelectTrigger>
456
+ <SelectContent>
457
+ {STATUS_FILTERS.map((f) => (
458
+ <SelectItem key={f.value} value={f.value}>
459
+ {f.label}
460
+ </SelectItem>
461
+ ))}
462
+ </SelectContent>
463
+ </Select>
464
+ <div className="relative max-w-sm">
465
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
466
+ <Input
467
+ placeholder="Search requests..."
468
+ value={search}
469
+ onChange={(e) => setSearch(e.target.value)}
470
+ className="pl-9 focus:border-terminal focus:ring-terminal/20"
471
+ />
472
+ </div>
473
+ </div>
474
+ <Button
475
+ variant="outline"
476
+ className="border-terminal/30 text-terminal hover:bg-terminal/10"
477
+ onClick={() => setDialogOpen(true)}
478
+ >
479
+ <Trash2 className="mr-1.5 size-3.5" />
480
+ Trigger Deletion
481
+ </Button>
482
+ </div>
483
+
484
+ <div className="rounded-sm border border-terminal/10 overflow-x-auto">
485
+ <Table>
486
+ <TableHeader>
487
+ <TableRow className="bg-secondary crt-scanlines">
488
+ <TableHead className="text-xs font-medium uppercase tracking-wider w-[140px]">
489
+ Tenant ID
490
+ </TableHead>
491
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
492
+ Requested By
493
+ </TableHead>
494
+ <TableHead className="text-xs font-medium uppercase tracking-wider">Status</TableHead>
495
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
496
+ Delete After
497
+ </TableHead>
498
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
499
+ Created
500
+ </TableHead>
501
+ <TableHead className="text-xs font-medium uppercase tracking-wider w-[80px]">
502
+ Actions
503
+ </TableHead>
504
+ </TableRow>
505
+ </TableHeader>
506
+ <TableBody
507
+ className={cn("transition-opacity duration-150", loading && data && "opacity-60")}
508
+ >
509
+ {loading && !data ? (
510
+ <SkeletonRows cols={6} />
511
+ ) : filteredRequests.length === 0 && !loading ? (
512
+ <EmptyState message="No deletion requests found" />
513
+ ) : (
514
+ filteredRequests.map((req) => (
515
+ <TableRow key={req.id} className="h-10 hover:bg-secondary/50">
516
+ <TableCell className="font-mono text-xs w-[140px]">{req.tenantId}</TableCell>
517
+ <TableCell className="font-mono text-xs">{req.requestedBy}</TableCell>
518
+ <TableCell>
519
+ <span
520
+ className={cn(
521
+ "inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium",
522
+ statusBadgeClasses(req.status),
523
+ )}
524
+ >
525
+ {req.status}
526
+ </span>
527
+ </TableCell>
528
+ <TableCell>
529
+ <Tooltip>
530
+ <TooltipTrigger className="text-xs text-muted-foreground font-mono">
531
+ {relativeTime(req.deleteAfter)}
532
+ </TooltipTrigger>
533
+ <TooltipContent>{new Date(req.deleteAfter).toLocaleString()}</TooltipContent>
534
+ </Tooltip>
535
+ </TableCell>
536
+ <TableCell>
537
+ <Tooltip>
538
+ <TooltipTrigger className="text-xs text-muted-foreground font-mono">
539
+ {relativeTime(req.createdAt)}
540
+ </TooltipTrigger>
541
+ <TooltipContent>{new Date(req.createdAt).toLocaleString()}</TooltipContent>
542
+ </Tooltip>
543
+ </TableCell>
544
+ <TableCell>
545
+ {req.status === "pending" ? (
546
+ <Button
547
+ variant="ghost"
548
+ size="sm"
549
+ className="text-xs"
550
+ onClick={() => handleCancelDeletion(req.id)}
551
+ >
552
+ Cancel
553
+ </Button>
554
+ ) : (
555
+ <span className="text-muted-foreground">{"\u2014"}</span>
556
+ )}
557
+ </TableCell>
558
+ </TableRow>
559
+ ))
560
+ )}
561
+ </TableBody>
562
+ </Table>
563
+ </div>
564
+
565
+ {data && (
566
+ <Pagination
567
+ offset={offset}
568
+ total={data.total}
569
+ hasMore={offset + PAGE_SIZE < data.total}
570
+ onNavigate={(o) => load(o)}
571
+ />
572
+ )}
573
+
574
+ <TriggerDialog
575
+ open={dialogOpen}
576
+ onOpenChange={setDialogOpen}
577
+ title="Trigger Data Deletion"
578
+ description="Schedule all personal data for deletion for a specific tenant. This action is audited."
579
+ confirmLabel="Confirm Deletion"
580
+ confirmClassName="border-destructive/30 text-red-400 hover:bg-destructive/10"
581
+ onConfirm={handleTriggerDeletion}
582
+ />
583
+ </div>
584
+ );
585
+ }
586
+
587
+ // ============================================================
588
+ // TAB 2: Data Exports
589
+ // ============================================================
590
+
591
+ function DataExportsTab() {
592
+ const [data, setData] = useState<{
593
+ requests: ExportRequest[];
594
+ total: number;
595
+ } | null>(null);
596
+ const [loading, setLoading] = useState(true);
597
+ const [loadError, setLoadError] = useState<string | null>(null);
598
+ const [offset, setOffset] = useState(0);
599
+ const [statusFilter, setStatusFilter] = useState("all");
600
+ const [search, setSearch] = useState("");
601
+ const [dialogOpen, setDialogOpen] = useState(false);
602
+
603
+ const load = useCallback(
604
+ async (newOffset: number, signal?: AbortSignal) => {
605
+ setLoading(true);
606
+ setLoadError(null);
607
+ try {
608
+ const result = await getExportRequests({
609
+ status: statusFilter === "all" ? undefined : statusFilter,
610
+ limit: PAGE_SIZE,
611
+ offset: newOffset,
612
+ });
613
+ if (signal?.aborted) return;
614
+ setData(result);
615
+ setOffset(newOffset);
616
+ } catch (err) {
617
+ if (signal?.aborted) return;
618
+ const message = err instanceof Error ? err.message : "Failed to load";
619
+ if (message.includes("NOT_IMPLEMENTED") || message.includes("not found")) {
620
+ setLoadError("backend_missing");
621
+ } else {
622
+ setLoadError(message);
623
+ }
624
+ } finally {
625
+ if (!signal?.aborted) setLoading(false);
626
+ }
627
+ },
628
+ [statusFilter],
629
+ );
630
+
631
+ useEffect(() => {
632
+ const controller = new AbortController();
633
+ load(0, controller.signal);
634
+ return () => controller.abort();
635
+ }, [load]);
636
+
637
+ const filteredRequests = useMemo(() => {
638
+ if (!data) return [];
639
+ if (!search.trim()) return data.requests;
640
+ const q = search.toLowerCase();
641
+ return data.requests.filter(
642
+ (r) =>
643
+ r.tenantId.toLowerCase().includes(q) ||
644
+ r.requestedBy.toLowerCase().includes(q) ||
645
+ r.status.toLowerCase().includes(q),
646
+ );
647
+ }, [data, search]);
648
+
649
+ const handleTriggerExport = async (tenantId: string, reason: string) => {
650
+ await triggerExport(tenantId, reason);
651
+ toast.success("Export request created");
652
+ load(offset);
653
+ };
654
+
655
+ // Backend endpoints not yet available
656
+ if (loadError === "backend_missing") {
657
+ return (
658
+ <div className="space-y-3">
659
+ <div className="flex flex-wrap items-center justify-between gap-2">
660
+ <div className="flex flex-wrap items-center gap-2">
661
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
662
+ <SelectTrigger className="w-[150px]">
663
+ <SelectValue />
664
+ </SelectTrigger>
665
+ <SelectContent>
666
+ {STATUS_FILTERS.map((f) => (
667
+ <SelectItem key={f.value} value={f.value}>
668
+ {f.label}
669
+ </SelectItem>
670
+ ))}
671
+ </SelectContent>
672
+ </Select>
673
+ <div className="relative max-w-sm">
674
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
675
+ <Input
676
+ placeholder="Search exports..."
677
+ value={search}
678
+ onChange={(e) => setSearch(e.target.value)}
679
+ className="pl-9 focus:border-terminal focus:ring-terminal/20"
680
+ disabled
681
+ />
682
+ </div>
683
+ </div>
684
+ <Button
685
+ variant="outline"
686
+ className="border-terminal/30 text-terminal hover:bg-terminal/10"
687
+ onClick={() => setDialogOpen(true)}
688
+ disabled
689
+ >
690
+ <Download className="mr-1.5 size-3.5" />
691
+ Trigger Export
692
+ </Button>
693
+ </div>
694
+ <div className="rounded-sm border border-terminal/10 p-8 text-center">
695
+ <p className="text-sm text-muted-foreground font-mono">
696
+ &gt; Backend endpoints for listing export requests are pending deployment
697
+ <span className="animate-ellipsis" />
698
+ </p>
699
+ <p className="mt-2 text-xs text-muted-foreground">
700
+ The admin.complianceExportRequests procedure is not yet available. This section will
701
+ activate automatically once deployed.
702
+ </p>
703
+ </div>
704
+ <TriggerDialog
705
+ open={dialogOpen}
706
+ onOpenChange={setDialogOpen}
707
+ title="Trigger Data Export"
708
+ description="Generate a GDPR Article 15 data export for a specific tenant. This action is audited."
709
+ confirmLabel="Confirm Export"
710
+ confirmClassName="border-terminal/30 text-terminal hover:bg-terminal/10"
711
+ onConfirm={handleTriggerExport}
712
+ />
713
+ </div>
714
+ );
715
+ }
716
+
717
+ if (loadError) {
718
+ return (
719
+ <div className="flex h-40 flex-col items-center justify-center gap-3">
720
+ <p className="text-sm text-destructive font-mono">Failed to load export requests.</p>
721
+ <Button variant="outline" size="sm" onClick={() => load(offset)}>
722
+ Retry
723
+ </Button>
724
+ </div>
725
+ );
726
+ }
727
+
728
+ return (
729
+ <div className="space-y-3">
730
+ <div className="flex flex-wrap items-center justify-between gap-2">
731
+ <div className="flex flex-wrap items-center gap-2">
732
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
733
+ <SelectTrigger className="w-[150px]">
734
+ <SelectValue />
735
+ </SelectTrigger>
736
+ <SelectContent>
737
+ {STATUS_FILTERS.map((f) => (
738
+ <SelectItem key={f.value} value={f.value}>
739
+ {f.label}
740
+ </SelectItem>
741
+ ))}
742
+ </SelectContent>
743
+ </Select>
744
+ <div className="relative max-w-sm">
745
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
746
+ <Input
747
+ placeholder="Search exports..."
748
+ value={search}
749
+ onChange={(e) => setSearch(e.target.value)}
750
+ className="pl-9 focus:border-terminal focus:ring-terminal/20"
751
+ />
752
+ </div>
753
+ </div>
754
+ <Button
755
+ variant="outline"
756
+ className="border-terminal/30 text-terminal hover:bg-terminal/10"
757
+ onClick={() => setDialogOpen(true)}
758
+ >
759
+ <Download className="mr-1.5 size-3.5" />
760
+ Trigger Export
761
+ </Button>
762
+ </div>
763
+
764
+ <div className="rounded-sm border border-terminal/10 overflow-x-auto">
765
+ <Table>
766
+ <TableHeader>
767
+ <TableRow className="bg-secondary crt-scanlines">
768
+ <TableHead className="text-xs font-medium uppercase tracking-wider w-[140px]">
769
+ Tenant ID
770
+ </TableHead>
771
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
772
+ Requested By
773
+ </TableHead>
774
+ <TableHead className="text-xs font-medium uppercase tracking-wider">Status</TableHead>
775
+ <TableHead className="text-xs font-medium uppercase tracking-wider">Format</TableHead>
776
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
777
+ Created
778
+ </TableHead>
779
+ <TableHead className="text-xs font-medium uppercase tracking-wider w-[80px]">
780
+ Actions
781
+ </TableHead>
782
+ </TableRow>
783
+ </TableHeader>
784
+ <TableBody
785
+ className={cn("transition-opacity duration-150", loading && data && "opacity-60")}
786
+ >
787
+ {loading && !data ? (
788
+ <SkeletonRows cols={6} />
789
+ ) : filteredRequests.length === 0 && !loading ? (
790
+ <EmptyState message="No export requests found" />
791
+ ) : (
792
+ filteredRequests.map((req) => (
793
+ <TableRow key={req.id} className="h-10 hover:bg-secondary/50">
794
+ <TableCell className="font-mono text-xs w-[140px]">{req.tenantId}</TableCell>
795
+ <TableCell className="font-mono text-xs">{req.requestedBy}</TableCell>
796
+ <TableCell>
797
+ <span
798
+ className={cn(
799
+ "inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium",
800
+ statusBadgeClasses(req.status),
801
+ )}
802
+ >
803
+ {req.status}
804
+ </span>
805
+ </TableCell>
806
+ <TableCell>
807
+ <span className="inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium bg-secondary text-muted-foreground border border-border">
808
+ {req.format.toUpperCase()}
809
+ </span>
810
+ </TableCell>
811
+ <TableCell>
812
+ <Tooltip>
813
+ <TooltipTrigger className="text-xs text-muted-foreground font-mono">
814
+ {relativeTime(req.createdAt)}
815
+ </TooltipTrigger>
816
+ <TooltipContent>{new Date(req.createdAt).toLocaleString()}</TooltipContent>
817
+ </Tooltip>
818
+ </TableCell>
819
+ <TableCell>
820
+ {req.status === "completed" && req.downloadUrl ? (
821
+ isSafeUrl(req.downloadUrl) ? (
822
+ <a
823
+ href={req.downloadUrl}
824
+ target="_blank"
825
+ rel="noopener noreferrer"
826
+ className="text-xs text-terminal hover:underline"
827
+ >
828
+ Download
829
+ </a>
830
+ ) : (
831
+ <span className="text-xs text-muted-foreground">Unavailable</span>
832
+ )
833
+ ) : (
834
+ <span className="text-muted-foreground">{"\u2014"}</span>
835
+ )}
836
+ </TableCell>
837
+ </TableRow>
838
+ ))
839
+ )}
840
+ </TableBody>
841
+ </Table>
842
+ </div>
843
+
844
+ {data && (
845
+ <Pagination
846
+ offset={offset}
847
+ total={data.total}
848
+ hasMore={offset + PAGE_SIZE < data.total}
849
+ onNavigate={(o) => load(o)}
850
+ />
851
+ )}
852
+
853
+ <TriggerDialog
854
+ open={dialogOpen}
855
+ onOpenChange={setDialogOpen}
856
+ title="Trigger Data Export"
857
+ description="Generate a GDPR Article 15 data export for a specific tenant. This action is audited."
858
+ confirmLabel="Confirm Export"
859
+ confirmClassName="border-terminal/30 text-terminal hover:bg-terminal/10"
860
+ onConfirm={handleTriggerExport}
861
+ />
862
+ </div>
863
+ );
864
+ }
865
+
866
+ // ============================================================
867
+ // TAB 3: Compliance Audit Trail
868
+ // ============================================================
869
+
870
+ function ComplianceAuditTab() {
871
+ const [data, setData] = useState<AuditLogResponse | null>(null);
872
+ const [loading, setLoading] = useState(true);
873
+ const [loadError, setLoadError] = useState(false);
874
+ const [offset, setOffset] = useState(0);
875
+ const [dateRange, setDateRange] = useState("30");
876
+ const [actionFilter, setActionFilter] = useState("all");
877
+ const [search, setSearch] = useState("");
878
+
879
+ const load = useCallback(
880
+ async (newOffset: number, signal?: AbortSignal) => {
881
+ setLoading(true);
882
+ setLoadError(false);
883
+ try {
884
+ const since =
885
+ dateRange === "all"
886
+ ? undefined
887
+ : new Date(Date.now() - Number(dateRange) * 86400000).toISOString();
888
+ const result = await fetchAuditLog({
889
+ limit: PAGE_SIZE,
890
+ offset: newOffset,
891
+ since,
892
+ action: actionFilter === "all" ? "compliance" : actionFilter,
893
+ });
894
+ if (signal?.aborted) return;
895
+ setData(result);
896
+ setOffset(newOffset);
897
+ } catch {
898
+ if (signal?.aborted) return;
899
+ setLoadError(true);
900
+ } finally {
901
+ if (!signal?.aborted) setLoading(false);
902
+ }
903
+ },
904
+ [dateRange, actionFilter],
905
+ );
906
+
907
+ useEffect(() => {
908
+ const controller = new AbortController();
909
+ load(0, controller.signal);
910
+ return () => controller.abort();
911
+ }, [load]);
912
+
913
+ const filteredEvents = useMemo(() => {
914
+ if (!data) return [];
915
+ if (!search.trim()) return data.events;
916
+ const q = search.toLowerCase();
917
+ return data.events.filter(
918
+ (e) =>
919
+ e.action.toLowerCase().includes(q) ||
920
+ e.resourceType.toLowerCase().includes(q) ||
921
+ (e.resourceName ?? e.resourceId).toLowerCase().includes(q) ||
922
+ (e.details ?? "").toLowerCase().includes(q),
923
+ );
924
+ }, [data, search]);
925
+
926
+ if (loadError) {
927
+ return (
928
+ <div className="flex h-40 flex-col items-center justify-center gap-3">
929
+ <p className="text-sm text-destructive font-mono">Failed to load compliance audit trail.</p>
930
+ <Button variant="outline" size="sm" onClick={() => load(offset)}>
931
+ Retry
932
+ </Button>
933
+ </div>
934
+ );
935
+ }
936
+
937
+ return (
938
+ <div className="space-y-3">
939
+ <div className="flex flex-wrap items-center gap-2">
940
+ <div className="relative max-w-sm">
941
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
942
+ <Input
943
+ placeholder="Search events..."
944
+ value={search}
945
+ onChange={(e) => setSearch(e.target.value)}
946
+ className="pl-9 focus:border-terminal focus:ring-terminal/20"
947
+ />
948
+ </div>
949
+ <Select value={dateRange} onValueChange={setDateRange}>
950
+ <SelectTrigger className="w-[150px]">
951
+ <SelectValue />
952
+ </SelectTrigger>
953
+ <SelectContent>
954
+ {DATE_RANGES.map((r) => (
955
+ <SelectItem key={r.value} value={r.value}>
956
+ {r.label}
957
+ </SelectItem>
958
+ ))}
959
+ </SelectContent>
960
+ </Select>
961
+ <Select value={actionFilter} onValueChange={setActionFilter}>
962
+ <SelectTrigger className="w-[160px]">
963
+ <SelectValue />
964
+ </SelectTrigger>
965
+ <SelectContent>
966
+ {AUDIT_ACTION_FILTERS.map((f) => (
967
+ <SelectItem key={f.value} value={f.value}>
968
+ {f.label}
969
+ </SelectItem>
970
+ ))}
971
+ </SelectContent>
972
+ </Select>
973
+ </div>
974
+
975
+ <div className="rounded-sm border border-terminal/10 overflow-x-auto">
976
+ <Table>
977
+ <TableHeader>
978
+ <TableRow className="bg-secondary crt-scanlines">
979
+ <TableHead className="text-xs font-medium uppercase tracking-wider w-[100px]">
980
+ Time
981
+ </TableHead>
982
+ <TableHead className="text-xs font-medium uppercase tracking-wider">Action</TableHead>
983
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
984
+ Resource
985
+ </TableHead>
986
+ <TableHead className="text-xs font-medium uppercase tracking-wider">
987
+ Details
988
+ </TableHead>
989
+ </TableRow>
990
+ </TableHeader>
991
+ <TableBody
992
+ className={cn("transition-opacity duration-150", loading && data && "opacity-60")}
993
+ >
994
+ {loading && !data ? (
995
+ <SkeletonRows cols={4} rows={8} />
996
+ ) : filteredEvents.length === 0 && !loading ? (
997
+ <EmptyState message="No compliance audit events found" />
998
+ ) : (
999
+ filteredEvents.map((event) => (
1000
+ <TableRow key={event.id} className="h-10 hover:bg-secondary/50">
1001
+ <TableCell className="w-[100px]">
1002
+ <Tooltip>
1003
+ <TooltipTrigger className="text-xs text-muted-foreground font-mono">
1004
+ {relativeTime(event.createdAt)}
1005
+ </TooltipTrigger>
1006
+ <TooltipContent>{new Date(event.createdAt).toLocaleString()}</TooltipContent>
1007
+ </Tooltip>
1008
+ </TableCell>
1009
+ <TableCell>
1010
+ <span
1011
+ className={cn(
1012
+ "inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium",
1013
+ complianceActionBadgeClasses(event.action),
1014
+ )}
1015
+ >
1016
+ {humanAction(event.action)}
1017
+ </span>
1018
+ </TableCell>
1019
+ <TableCell className="text-sm">
1020
+ <span className="text-xs uppercase tracking-wide text-muted-foreground">
1021
+ {event.resourceType}
1022
+ </span>{" "}
1023
+ <span className="font-mono text-xs">
1024
+ {event.resourceName ?? event.resourceId}
1025
+ </span>
1026
+ </TableCell>
1027
+ <TableCell className="text-xs text-muted-foreground max-w-[300px] truncate">
1028
+ {event.details ?? "\u2014"}
1029
+ </TableCell>
1030
+ </TableRow>
1031
+ ))
1032
+ )}
1033
+ </TableBody>
1034
+ </Table>
1035
+ </div>
1036
+
1037
+ {data && (
1038
+ <Pagination
1039
+ offset={offset}
1040
+ total={data.total}
1041
+ hasMore={data.hasMore}
1042
+ onNavigate={(o) => load(o)}
1043
+ />
1044
+ )}
1045
+ </div>
1046
+ );
1047
+ }
1048
+
1049
+ // ============================================================
1050
+ // TAB 4: Retention Policies
1051
+ // ============================================================
1052
+
1053
+ function EditRetentionPolicyDialog({
1054
+ policy,
1055
+ open,
1056
+ onOpenChange,
1057
+ onSaved,
1058
+ }: {
1059
+ policy: RetentionPolicy;
1060
+ open: boolean;
1061
+ onOpenChange: (open: boolean) => void;
1062
+ onSaved: (updated: RetentionPolicy) => void;
1063
+ }) {
1064
+ const [retentionDaysInput, setRetentionDaysInput] = useState(String(policy.retentionDays));
1065
+ const [autoDelete, setAutoDelete] = useState(policy.autoDelete);
1066
+ const [saving, setSaving] = useState(false);
1067
+
1068
+ const retentionDaysParsed = parseInt(retentionDaysInput, 10);
1069
+ const retentionDaysValid =
1070
+ !Number.isNaN(retentionDaysParsed) && retentionDaysParsed >= 1 && retentionDaysParsed <= 3650;
1071
+
1072
+ useEffect(() => {
1073
+ if (open) {
1074
+ setRetentionDaysInput(String(policy.retentionDays));
1075
+ setAutoDelete(policy.autoDelete);
1076
+ }
1077
+ }, [open, policy.retentionDays, policy.autoDelete]);
1078
+
1079
+ const handleSave = async () => {
1080
+ if (!retentionDaysValid) return;
1081
+ setSaving(true);
1082
+ try {
1083
+ const updated = await updateRetentionPolicy(policy.dataType, {
1084
+ retentionDays: retentionDaysParsed,
1085
+ autoDelete,
1086
+ });
1087
+ onSaved(updated);
1088
+ onOpenChange(false);
1089
+ toast.success(`Updated retention policy for ${policy.dataType}`);
1090
+ } catch (err) {
1091
+ toast.error(toUserMessage(err));
1092
+ } finally {
1093
+ setSaving(false);
1094
+ }
1095
+ };
1096
+
1097
+ return (
1098
+ <Dialog open={open} onOpenChange={onOpenChange}>
1099
+ <DialogContent>
1100
+ <DialogHeader>
1101
+ <DialogTitle className="text-base font-semibold uppercase tracking-wider">
1102
+ Edit Retention Policy
1103
+ </DialogTitle>
1104
+ <DialogDescription>Update retention settings for {policy.dataType}</DialogDescription>
1105
+ </DialogHeader>
1106
+ <div className="space-y-4 py-2">
1107
+ <div className="space-y-2">
1108
+ <Label htmlFor="retentionDays">Retention period (days)</Label>
1109
+ <Input
1110
+ id="retentionDays"
1111
+ type="number"
1112
+ step={1}
1113
+ min={1}
1114
+ max={3650}
1115
+ value={retentionDaysInput}
1116
+ onChange={(e) => setRetentionDaysInput(e.target.value)}
1117
+ />
1118
+ </div>
1119
+ <div className="flex items-center justify-between">
1120
+ <Label htmlFor="autoDelete">Auto-delete</Label>
1121
+ <Switch id="autoDelete" checked={autoDelete} onCheckedChange={setAutoDelete} />
1122
+ </div>
1123
+ </div>
1124
+ <DialogFooter>
1125
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
1126
+ Cancel
1127
+ </Button>
1128
+ <Button onClick={handleSave} disabled={saving || !retentionDaysValid}>
1129
+ {saving ? "Saving\u2026" : "Save"}
1130
+ </Button>
1131
+ </DialogFooter>
1132
+ </DialogContent>
1133
+ </Dialog>
1134
+ );
1135
+ }
1136
+
1137
+ function RetentionPoliciesTab() {
1138
+ const [policies, setPolicies] = useState<RetentionPolicy[] | null>(null);
1139
+ const [loading, setLoading] = useState(true);
1140
+ const [loadError, setLoadError] = useState(false);
1141
+ const [editingPolicy, setEditingPolicy] = useState<RetentionPolicy | null>(null);
1142
+ const lastEditingPolicyRef = useRef<RetentionPolicy | null>(null);
1143
+ if (editingPolicy !== null) lastEditingPolicyRef.current = editingPolicy;
1144
+
1145
+ useEffect(() => {
1146
+ let cancelled = false;
1147
+ setLoading(true);
1148
+ setLoadError(false);
1149
+ fetchRetentionPolicies()
1150
+ .then((result) => {
1151
+ if (!cancelled) setPolicies(result);
1152
+ })
1153
+ .catch(() => {
1154
+ if (!cancelled) setLoadError(true);
1155
+ })
1156
+ .finally(() => {
1157
+ if (!cancelled) setLoading(false);
1158
+ });
1159
+ return () => {
1160
+ cancelled = true;
1161
+ };
1162
+ }, []);
1163
+
1164
+ if (loadError) {
1165
+ return (
1166
+ <div className="flex h-40 flex-col items-center justify-center gap-3">
1167
+ <p className="text-sm text-destructive font-mono">Failed to load retention policies.</p>
1168
+ <Button
1169
+ variant="outline"
1170
+ size="sm"
1171
+ onClick={() => {
1172
+ setLoading(true);
1173
+ setLoadError(false);
1174
+ fetchRetentionPolicies()
1175
+ .then(setPolicies)
1176
+ .catch(() => setLoadError(true))
1177
+ .finally(() => setLoading(false));
1178
+ }}
1179
+ >
1180
+ Retry
1181
+ </Button>
1182
+ </div>
1183
+ );
1184
+ }
1185
+
1186
+ if (loading) {
1187
+ return (
1188
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
1189
+ {SKEL_ROWS.slice(0, 6).map((key, i) => (
1190
+ <div key={key} className="rounded-sm border border-terminal/10 bg-card p-4 space-y-3">
1191
+ <Skeleton className="h-4 w-2/3" style={{ animationDelay: `${i * 50}ms` }} />
1192
+ <Skeleton className="h-3 w-full" style={{ animationDelay: `${i * 50 + 25}ms` }} />
1193
+ <Skeleton className="h-3 w-1/2" style={{ animationDelay: `${i * 50 + 50}ms` }} />
1194
+ </div>
1195
+ ))}
1196
+ </div>
1197
+ );
1198
+ }
1199
+
1200
+ if (!policies || policies.length === 0) {
1201
+ return (
1202
+ <div className="rounded-sm border border-terminal/10 p-8 text-center">
1203
+ <p className="text-sm text-muted-foreground font-mono">
1204
+ &gt; No retention policies configured
1205
+ <span className="animate-ellipsis" />
1206
+ </p>
1207
+ </div>
1208
+ );
1209
+ }
1210
+
1211
+ return (
1212
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
1213
+ {policies.map((policy) => (
1214
+ <div
1215
+ key={policy.dataType}
1216
+ className="rounded-sm border border-terminal/10 bg-card p-4 transition-colors duration-150 hover:border-terminal/20"
1217
+ >
1218
+ <h3 className="text-sm font-semibold uppercase tracking-wider text-foreground">
1219
+ {policy.dataType}
1220
+ </h3>
1221
+ <dl className="mt-3 space-y-1.5 text-xs font-mono text-muted-foreground">
1222
+ <div className="flex justify-between">
1223
+ <dt>Retention period</dt>
1224
+ <dd className="text-terminal font-semibold">{policy.retentionDays} days</dd>
1225
+ </div>
1226
+ <div className="flex justify-between">
1227
+ <dt>Auto-delete</dt>
1228
+ <dd>
1229
+ {policy.autoDelete ? (
1230
+ <span className="inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium bg-terminal/15 text-terminal border border-terminal/20">
1231
+ Enabled
1232
+ </span>
1233
+ ) : (
1234
+ <span className="inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-medium bg-secondary text-muted-foreground border border-border">
1235
+ Disabled
1236
+ </span>
1237
+ )}
1238
+ </dd>
1239
+ </div>
1240
+ <div className="flex justify-between">
1241
+ <dt>Last purge</dt>
1242
+ <dd>
1243
+ {policy.lastPurge ? (
1244
+ <Tooltip>
1245
+ <TooltipTrigger>{relativeTime(policy.lastPurge)}</TooltipTrigger>
1246
+ <TooltipContent>{new Date(policy.lastPurge).toLocaleString()}</TooltipContent>
1247
+ </Tooltip>
1248
+ ) : (
1249
+ "\u2014"
1250
+ )}
1251
+ </dd>
1252
+ </div>
1253
+ <div className="flex justify-between">
1254
+ <dt>Records affected</dt>
1255
+ <dd>{policy.recordsAffected.toLocaleString()}</dd>
1256
+ </div>
1257
+ </dl>
1258
+ <div className="mt-3 flex justify-end">
1259
+ <Button variant="outline" size="sm" onClick={() => setEditingPolicy(policy)}>
1260
+ <Pencil className="mr-1.5 size-3" />
1261
+ Edit
1262
+ </Button>
1263
+ </div>
1264
+ </div>
1265
+ ))}
1266
+ {lastEditingPolicyRef.current && (
1267
+ <EditRetentionPolicyDialog
1268
+ policy={lastEditingPolicyRef.current}
1269
+ open={editingPolicy !== null}
1270
+ onOpenChange={(open) => {
1271
+ if (!open) setEditingPolicy(null);
1272
+ }}
1273
+ onSaved={(updated) => {
1274
+ setPolicies((prev) =>
1275
+ prev ? prev.map((p) => (p.dataType === updated.dataType ? updated : p)) : prev,
1276
+ );
1277
+ setEditingPolicy(null);
1278
+ }}
1279
+ />
1280
+ )}
1281
+ </div>
1282
+ );
1283
+ }
1284
+
1285
+ // ============================================================
1286
+ // Main Dashboard
1287
+ // ============================================================
1288
+
1289
+ export function ComplianceDashboard() {
1290
+ const [activeTab, setActiveTab] = useState("deletions");
1291
+
1292
+ return (
1293
+ <div className="space-y-3 p-6">
1294
+ <div className="flex items-center justify-between">
1295
+ <h1 className="text-xl font-bold uppercase tracking-wider text-terminal [text-shadow:0_0_10px_rgba(0,255,65,0.25)]">
1296
+ GDPR COMPLIANCE
1297
+ </h1>
1298
+ <span className="text-sm text-muted-foreground font-mono">
1299
+ {activeTab === "deletions" && "Deletion requests"}
1300
+ {activeTab === "exports" && "Data exports"}
1301
+ {activeTab === "audit" && "Audit trail"}
1302
+ {activeTab === "retention" && "Retention policies"}
1303
+ </span>
1304
+ </div>
1305
+
1306
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
1307
+ <TabsList>
1308
+ <TabsTrigger value="deletions">
1309
+ <Trash2 className="mr-1.5 size-3.5" />
1310
+ Deletion Requests
1311
+ </TabsTrigger>
1312
+ <TabsTrigger value="exports">
1313
+ <Download className="mr-1.5 size-3.5" />
1314
+ Data Exports
1315
+ </TabsTrigger>
1316
+ <TabsTrigger value="audit">
1317
+ <FileText className="mr-1.5 size-3.5" />
1318
+ Audit Trail
1319
+ </TabsTrigger>
1320
+ <TabsTrigger value="retention">
1321
+ <Shield className="mr-1.5 size-3.5" />
1322
+ Retention Policies
1323
+ </TabsTrigger>
1324
+ </TabsList>
1325
+
1326
+ <TabsContent value="deletions">
1327
+ <DeletionRequestsTab />
1328
+ </TabsContent>
1329
+ <TabsContent value="exports">
1330
+ <DataExportsTab />
1331
+ </TabsContent>
1332
+ <TabsContent value="audit">
1333
+ <ComplianceAuditTab />
1334
+ </TabsContent>
1335
+ <TabsContent value="retention">
1336
+ <RetentionPoliciesTab />
1337
+ </TabsContent>
1338
+ </Tabs>
1339
+ </div>
1340
+ );
1341
+ }