auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,584 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { AlertTriangle, Copy, RefreshCw } from 'lucide-react';
5
+ import { api, Api, type DashboardResponse } from '@/lib/api';
6
+ import { Button, FilterDropdown } from '@/components/design-system';
7
+ import { useWebSocket } from '@/context/WebSocketContext';
8
+ import { WALLET_EVENTS } from '@/lib/events';
9
+ import {
10
+ dedupeAuditEvents,
11
+ fromLegacyLog,
12
+ fromTask40Row,
13
+ type UiAuditEvent,
14
+ type UiDecision,
15
+ } from '@/lib/audit-console-adapter';
16
+
17
+ type DashboardToken = DashboardResponse['tokens']['active'][number] & {
18
+ createdAt?: string;
19
+ };
20
+ type TokenStatusFilter = 'active' | 'revoked' | 'expired' | 'all';
21
+ type TokenStatus = 'active' | 'revoked' | 'expired' | 'inactive';
22
+ type TimeWindow = '1h' | '24h' | '7d';
23
+
24
+ const PAGE_SIZE = 50;
25
+ const HARD_CAP = 500;
26
+ const STATUS_OPTIONS: { value: TokenStatusFilter; label: string }[] = [
27
+ { value: 'active', label: 'ACTIVE' },
28
+ { value: 'revoked', label: 'REVOKED' },
29
+ { value: 'expired', label: 'EXPIRED' },
30
+ { value: 'all', label: 'ALL' },
31
+ ];
32
+ const WINDOW_OPTIONS: { value: TimeWindow; label: string }[] = [
33
+ { value: '1h', label: '1 HOUR' },
34
+ { value: '24h', label: '24 HOURS' },
35
+ { value: '7d', label: '7 DAYS' },
36
+ ];
37
+ const DECISION_OPTIONS: { value: UiDecision | 'all'; label: string }[] = [
38
+ { value: 'all', label: 'ALL' },
39
+ { value: 'ALLOW', label: 'ALLOW' },
40
+ { value: 'DENY', label: 'DENY' },
41
+ { value: 'RATE_LIMIT', label: 'RATE_LIMIT' },
42
+ { value: 'ERROR', label: 'ERROR' },
43
+ { value: 'UNKNOWN', label: 'UNKNOWN' },
44
+ ];
45
+
46
+ function toMs(value: string | number | undefined): number | null {
47
+ if (value == null) return null;
48
+ if (typeof value === 'number') return value < 1_000_000_000_000 ? value * 1000 : value;
49
+ const parsed = Date.parse(value);
50
+ return Number.isFinite(parsed) ? parsed : null;
51
+ }
52
+
53
+ function toWindowMs(window: TimeWindow): number {
54
+ if (window === '1h') return 60 * 60 * 1000;
55
+ if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
56
+ return 24 * 60 * 60 * 1000;
57
+ }
58
+
59
+ function classifyTokenStatus(token: DashboardToken): TokenStatus {
60
+ if (token.isRevoked) return 'revoked';
61
+ const expiresAtMs = toMs(token.expiresAt);
62
+ if (token.isExpired || (expiresAtMs !== null && expiresAtMs <= Date.now())) return 'expired';
63
+ if (token.isActive) return 'active';
64
+ return 'inactive';
65
+ }
66
+
67
+ function shortHash(hash: string | undefined): string {
68
+ if (!hash) return '—';
69
+ if (hash.length <= 14) return hash;
70
+ return `${hash.slice(0, 8)}...${hash.slice(-6)}`;
71
+ }
72
+
73
+ function formatDate(value: string | number | undefined): string {
74
+ const ms = toMs(value);
75
+ if (ms === null) return '—';
76
+ return new Date(ms).toLocaleString();
77
+ }
78
+
79
+ function formatRelative(value: string | number | undefined): string {
80
+ const ms = toMs(value);
81
+ if (ms === null) return '—';
82
+ const diff = ms - Date.now();
83
+ if (diff <= 0) return 'expired';
84
+ const mins = Math.floor(diff / 60000);
85
+ if (mins < 60) return `${mins}m`;
86
+ const hours = Math.floor(mins / 60);
87
+ if (hours < 24) return `${hours}h`;
88
+ return `${Math.floor(hours / 24)}d`;
89
+ }
90
+
91
+ function statusTone(status: TokenStatus): string {
92
+ if (status === 'active') return 'text-[var(--color-success,#16a34a)]';
93
+ if (status === 'revoked') return 'text-[var(--color-warning,#ff4d00)]';
94
+ if (status === 'expired') return 'text-[var(--color-text-faint,#9ca3af)]';
95
+ return 'text-[var(--color-text-muted,#6b7280)]';
96
+ }
97
+
98
+ function permissionsSummary(permissions: string[]): string {
99
+ if (!permissions || permissions.length === 0) return '—';
100
+ if (permissions.length <= 2) return permissions.join(', ');
101
+ return `${permissions.slice(0, 2).join(', ')} +${permissions.length - 2}`;
102
+ }
103
+
104
+ export function AuditConsole(): React.JSX.Element {
105
+ const { subscribe } = useWebSocket();
106
+ const [tokens, setTokens] = useState<DashboardToken[]>([]);
107
+ const [events, setEvents] = useState<UiAuditEvent[]>([]);
108
+ const [statusFilter, setStatusFilter] = useState<TokenStatusFilter>('active');
109
+ const [timeWindow, setTimeWindow] = useState<TimeWindow>('24h');
110
+ const [searchQuery, setSearchQuery] = useState('');
111
+ const [selectedTokenKey, setSelectedTokenKey] = useState<string>('all');
112
+ const [decisionFilter, setDecisionFilter] = useState<UiDecision | 'all'>('all');
113
+ const [page, setPage] = useState(0);
114
+ const [loading, setLoading] = useState(true);
115
+ const [error, setError] = useState<string | null>(null);
116
+ const [notice, setNotice] = useState<string | null>(null);
117
+ const [revokingTokenKey, setRevokingTokenKey] = useState<string | null>(null);
118
+ const [copiedTokenKey, setCopiedTokenKey] = useState<string | null>(null);
119
+ const activitySectionRef = useRef<HTMLDivElement | null>(null);
120
+ const liveRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
121
+
122
+ const fetchAll = useCallback(async () => {
123
+ setLoading(true);
124
+ setError(null);
125
+ try {
126
+ const [dashboard, access, legacy] = await Promise.all([
127
+ api.get<{ success: boolean; tokens: { active: DashboardToken[]; inactive: DashboardToken[] } }>(Api.Wallet, '/dashboard'),
128
+ api.get<{ success: boolean; rows: Record<string, unknown>[] }>(Api.Wallet, '/security/credential-access/recent', { limit: HARD_CAP }),
129
+ api.get<{ success: boolean; logs: Record<string, unknown>[] }>(Api.Wallet, '/logs', { category: 'agent', limit: 200 }),
130
+ ]);
131
+
132
+ const dashboardTokens = dashboard.success
133
+ ? [...(dashboard.tokens.active ?? []), ...(dashboard.tokens.inactive ?? [])]
134
+ : [];
135
+
136
+ const task40Rows = access.success ? access.rows.map(fromTask40Row) : [];
137
+ const legacyRows = legacy.success
138
+ ? legacy.logs
139
+ .filter((row) => {
140
+ const data = (row.data ?? {}) as Record<string, unknown>;
141
+ return data.action === 'credential_access_decision';
142
+ })
143
+ .map(fromLegacyLog)
144
+ : [];
145
+
146
+ const now = Date.now();
147
+ const merged = dedupeAuditEvents([...task40Rows, ...legacyRows]).filter((row) => row.timestamp <= now);
148
+
149
+ setTokens(dashboardTokens);
150
+ setEvents(merged.slice(0, HARD_CAP));
151
+ setPage(0);
152
+ } catch (err) {
153
+ setError(err instanceof Error ? err.message : 'Failed to load audit console');
154
+ } finally {
155
+ setLoading(false);
156
+ }
157
+ }, []);
158
+
159
+ useEffect(() => {
160
+ void fetchAll();
161
+ }, [fetchAll]);
162
+
163
+ const scheduleLiveRefresh = useCallback(() => {
164
+ if (liveRefreshTimerRef.current) return;
165
+ liveRefreshTimerRef.current = setTimeout(() => {
166
+ liveRefreshTimerRef.current = null;
167
+ void fetchAll();
168
+ }, 120);
169
+ }, [fetchAll]);
170
+
171
+ useEffect(() => {
172
+ const unsubs = [
173
+ subscribe(WALLET_EVENTS.TOKEN_CREATED, scheduleLiveRefresh),
174
+ subscribe(WALLET_EVENTS.TOKEN_REVOKED, scheduleLiveRefresh),
175
+ subscribe(WALLET_EVENTS.TOKEN_SPENT, scheduleLiveRefresh),
176
+ subscribe(WALLET_EVENTS.CREDENTIAL_CHANGED, scheduleLiveRefresh),
177
+ subscribe(WALLET_EVENTS.CREDENTIAL_ACCESSED, scheduleLiveRefresh),
178
+ ];
179
+
180
+ return () => {
181
+ unsubs.forEach((unsub) => unsub());
182
+ if (liveRefreshTimerRef.current) {
183
+ clearTimeout(liveRefreshTimerRef.current);
184
+ liveRefreshTimerRef.current = null;
185
+ }
186
+ };
187
+ }, [scheduleLiveRefresh, subscribe]);
188
+
189
+ useEffect(() => {
190
+ setPage(0);
191
+ }, [selectedTokenKey, decisionFilter, timeWindow, searchQuery]);
192
+
193
+ const cutoff = useMemo(() => Date.now() - toWindowMs(timeWindow), [timeWindow]);
194
+ const normalizedSearch = searchQuery.trim().toLowerCase();
195
+
196
+ const tokenRows = useMemo(() => {
197
+ return tokens.map((token, idx) => {
198
+ const key = token.tokenHash || token.agentId || `unknown-${idx}`;
199
+ return { key, token, status: classifyTokenStatus(token) };
200
+ });
201
+ }, [tokens]);
202
+
203
+ const tokenRowsByKey = useMemo(() => new Map(tokenRows.map((row) => [row.key, row])), [tokenRows]);
204
+ const selectedTokenRow = selectedTokenKey === 'all' ? null : tokenRowsByKey.get(selectedTokenKey) ?? null;
205
+
206
+ const windowedEvents = useMemo(
207
+ () => events.filter((row) => row.timestamp >= cutoff),
208
+ [events, cutoff],
209
+ );
210
+
211
+ const summary = useMemo(() => {
212
+ const activeCount = tokenRows.filter((row) => row.status === 'active').length;
213
+ const denied = windowedEvents.filter((row) => row.decision === 'DENY').length;
214
+ const rateLimited = windowedEvents.filter((row) => row.decision === 'RATE_LIMIT').length;
215
+ const unknown = windowedEvents.filter((row) => row.decision === 'UNKNOWN' || row.reasonCode === 'UNKNOWN').length;
216
+ return { activeCount, denied, rateLimited, unknown };
217
+ }, [tokenRows, windowedEvents]);
218
+
219
+ const filteredTokenRows = useMemo(() => {
220
+ return tokenRows.filter((row) => {
221
+ if (statusFilter !== 'all' && row.status !== statusFilter) return false;
222
+ if (!normalizedSearch) return true;
223
+ const haystack = `${row.key} ${row.token.tokenHash} ${row.token.agentId}`.toLowerCase();
224
+ return haystack.includes(normalizedSearch);
225
+ });
226
+ }, [tokenRows, statusFilter, normalizedSearch]);
227
+
228
+ const tokenOptions = useMemo(() => {
229
+ const options: Array<{ value: string; label: string }> = [{ value: 'all', label: 'ALL TOKENS' }];
230
+ const seen = new Set<string>(['all']);
231
+ filteredTokenRows.forEach((row) => {
232
+ if (seen.has(row.key)) return;
233
+ seen.add(row.key);
234
+ options.push({ value: row.key, label: `${row.token.agentId ?? 'unknown'} · ${shortHash(row.token.tokenHash ?? row.key)}` });
235
+ });
236
+
237
+ if (selectedTokenKey !== 'all' && !seen.has(selectedTokenKey)) {
238
+ const row = tokenRowsByKey.get(selectedTokenKey);
239
+ if (row) options.push({ value: row.key, label: `${row.token.agentId ?? 'unknown'} · ${shortHash(row.token.tokenHash ?? row.key)}` });
240
+ }
241
+
242
+ return options;
243
+ }, [filteredTokenRows, selectedTokenKey, tokenRowsByKey]);
244
+
245
+ const lastUsedByKey = useMemo(() => {
246
+ const map = new Map<string, number>();
247
+ windowedEvents.forEach((row) => {
248
+ const candidates = [
249
+ row.tokenHash,
250
+ row.tokenKey,
251
+ row.agentId ? `agent:${row.agentId}` : undefined,
252
+ row.agentId,
253
+ ].filter((v): v is string => Boolean(v));
254
+
255
+ candidates.forEach((key) => {
256
+ map.set(key, Math.max(map.get(key) ?? 0, row.timestamp));
257
+ });
258
+ });
259
+ return map;
260
+ }, [windowedEvents]);
261
+
262
+ const getLastUsed = useCallback(
263
+ (row: { key: string; token: DashboardToken }): number | null => {
264
+ const tokenHash = row.token.tokenHash;
265
+ if (tokenHash && lastUsedByKey.has(tokenHash)) return lastUsedByKey.get(tokenHash) ?? null;
266
+ if (lastUsedByKey.has(row.key)) return lastUsedByKey.get(row.key) ?? null;
267
+ if (row.token.agentId && lastUsedByKey.has(`agent:${row.token.agentId}`)) {
268
+ return lastUsedByKey.get(`agent:${row.token.agentId}`) ?? null;
269
+ }
270
+ if (row.token.agentId && lastUsedByKey.has(row.token.agentId)) return lastUsedByKey.get(row.token.agentId) ?? null;
271
+ return null;
272
+ },
273
+ [lastUsedByKey],
274
+ );
275
+
276
+ const filtered = useMemo(() => {
277
+ return windowedEvents.filter((row) => {
278
+ if (selectedTokenKey !== 'all') {
279
+ const selectedAgentId = selectedTokenRow?.token.agentId;
280
+ const tokenMatch = row.tokenKey === selectedTokenKey || row.tokenHash === selectedTokenKey;
281
+ const agentMatch = selectedAgentId ? row.agentId === selectedAgentId : false;
282
+ if (!tokenMatch && !agentMatch) return false;
283
+ }
284
+ if (decisionFilter !== 'all' && row.decision !== decisionFilter) return false;
285
+ if (normalizedSearch) {
286
+ const haystack = `${row.tokenHash ?? ''} ${row.tokenKey} ${row.agentId ?? ''}`.toLowerCase();
287
+ if (!haystack.includes(normalizedSearch)) return false;
288
+ }
289
+ return true;
290
+ });
291
+ }, [decisionFilter, windowedEvents, selectedTokenKey, selectedTokenRow, normalizedSearch]);
292
+
293
+ const pagedRows = useMemo(() => {
294
+ const start = page * PAGE_SIZE;
295
+ return filtered.slice(start, start + PAGE_SIZE);
296
+ }, [filtered, page]);
297
+
298
+ const handleOpenActivity = useCallback((tokenKey: string) => {
299
+ setSelectedTokenKey(tokenKey);
300
+ setPage(0);
301
+ if (activitySectionRef.current && typeof activitySectionRef.current.scrollIntoView === 'function') {
302
+ activitySectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
303
+ }
304
+ }, []);
305
+
306
+ const handleCopyShortHash = useCallback(async (tokenKey: string, tokenHash: string | undefined) => {
307
+ if (!tokenHash || !navigator.clipboard) {
308
+ setNotice('Clipboard unavailable for this key.');
309
+ return;
310
+ }
311
+ try {
312
+ const value = shortHash(tokenHash);
313
+ await navigator.clipboard.writeText(value);
314
+ setCopiedTokenKey(tokenKey);
315
+ setNotice(`Copied ${value}`);
316
+ setTimeout(() => setCopiedTokenKey((prev) => (prev === tokenKey ? null : prev)), 1500);
317
+ } catch {
318
+ setNotice('Clipboard write failed.');
319
+ }
320
+ }, []);
321
+
322
+ const handleRevoke = useCallback(async (tokenKey: string, tokenHash: string | undefined) => {
323
+ if (!tokenHash) {
324
+ setNotice('Token hash unavailable; cannot revoke.');
325
+ return;
326
+ }
327
+ if (!window.confirm(`Revoke token ${shortHash(tokenHash)}?`)) return;
328
+
329
+ setRevokingTokenKey(tokenKey);
330
+ setNotice(null);
331
+ try {
332
+ const result = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/actions/tokens/revoke', { tokenHash });
333
+ if (!result.success) {
334
+ setNotice(result.error || 'Failed to revoke token.');
335
+ return;
336
+ }
337
+ setNotice(`Revoked ${shortHash(tokenHash)}`);
338
+ await fetchAll();
339
+ } catch (err) {
340
+ setNotice(err instanceof Error ? err.message : 'Failed to revoke token.');
341
+ } finally {
342
+ setRevokingTokenKey(null);
343
+ }
344
+ }, [fetchAll]);
345
+
346
+ if (loading) {
347
+ return <div className="p-4 font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">LOADING_AUDIT_CONSOLE…</div>;
348
+ }
349
+
350
+ if (error) {
351
+ return (
352
+ <div className="p-4 border border-[var(--color-warning,#ff4d00)] bg-[color-mix(in_srgb,var(--color-warning,#ff4d00)_10%,transparent)]">
353
+ <div className="flex items-center gap-2 font-mono text-[10px] text-[var(--color-warning,#ff4d00)]">
354
+ <AlertTriangle size={12} /> {error}
355
+ </div>
356
+ </div>
357
+ );
358
+ }
359
+
360
+ return (
361
+ <div className="h-full flex flex-col p-3 gap-3 overflow-hidden">
362
+ <div className="flex items-center justify-between border border-[var(--color-border,#d4d4d8)] p-2 bg-[var(--color-surface,#fff)]">
363
+ <div>
364
+ <div className="font-mono text-[11px] font-bold">AUDIT</div>
365
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">
366
+ {WINDOW_OPTIONS.find((opt) => opt.value === timeWindow)?.label.toLowerCase()} · {Math.min(events.length, HARD_CAP)} rows
367
+ {events.length >= HARD_CAP ? ' (capped)' : ''}
368
+ </div>
369
+ </div>
370
+ <Button variant="secondary" size="sm" onClick={() => void fetchAll()} icon={<RefreshCw size={10} />}>
371
+ REFRESH
372
+ </Button>
373
+ </div>
374
+
375
+ <div className="grid grid-cols-2 xl:grid-cols-4 gap-2">
376
+ <div className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
377
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">ACTIVE KEYS</div>
378
+ <div className="font-mono text-[12px] font-bold">{summary.activeCount}</div>
379
+ </div>
380
+ <div className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
381
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">DENIES ({timeWindow})</div>
382
+ <div className="font-mono text-[12px] font-bold">{summary.denied}</div>
383
+ </div>
384
+ <div className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
385
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">RATE LIMITED ({timeWindow})</div>
386
+ <div className="font-mono text-[12px] font-bold">{summary.rateLimited}</div>
387
+ </div>
388
+ <div className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
389
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">UNKNOWN MAPS ({timeWindow})</div>
390
+ <div className="font-mono text-[12px] font-bold">{summary.unknown}</div>
391
+ </div>
392
+ </div>
393
+
394
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-2">
395
+ <FilterDropdown
396
+ label="Status"
397
+ options={STATUS_OPTIONS}
398
+ value={statusFilter}
399
+ onChange={(value) => setStatusFilter(value as TokenStatusFilter)}
400
+ compact
401
+ />
402
+
403
+ <FilterDropdown
404
+ label="Window"
405
+ options={WINDOW_OPTIONS}
406
+ value={timeWindow}
407
+ onChange={(value) => setTimeWindow(value as TimeWindow)}
408
+ compact
409
+ />
410
+
411
+ <FilterDropdown
412
+ label="Token"
413
+ options={tokenOptions}
414
+ value={selectedTokenKey}
415
+ onChange={setSelectedTokenKey}
416
+ compact
417
+ />
418
+
419
+ <FilterDropdown
420
+ label="Decision"
421
+ options={DECISION_OPTIONS}
422
+ value={decisionFilter}
423
+ onChange={(value) => setDecisionFilter(value as UiDecision | 'all')}
424
+ compact
425
+ />
426
+
427
+ <label className="font-mono text-[9px] flex flex-col gap-1">
428
+ SEARCH KEY / AGENT
429
+ <input
430
+ value={searchQuery}
431
+ onChange={(event) => setSearchQuery(event.target.value)}
432
+ placeholder="hash or agent id"
433
+ className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] px-2 py-1.5 text-[10px]"
434
+ />
435
+ </label>
436
+ </div>
437
+
438
+ {notice && (
439
+ <div className="font-mono text-[9px] border border-[var(--color-info,#0047ff)] p-2 text-[var(--color-info,#0047ff)] bg-[color-mix(in_srgb,var(--color-info,#0047ff)_8%,transparent)]">
440
+ {notice}
441
+ </div>
442
+ )}
443
+
444
+ <div className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] overflow-hidden">
445
+ <div className="px-2 py-1.5 border-b border-[var(--color-border,#d4d4d8)] font-mono text-[9px]">
446
+ KEY INVENTORY · {filteredTokenRows.length} ROWS
447
+ </div>
448
+ <div className="max-h-[220px] overflow-auto">
449
+ {filteredTokenRows.length === 0 ? (
450
+ <div className="p-4 font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">NO TOKENS MATCH FILTERS</div>
451
+ ) : (
452
+ <table className="w-full text-left">
453
+ <thead className="sticky top-0 bg-[var(--color-surface-alt,#fafafa)] border-b">
454
+ <tr className="font-mono text-[9px]">
455
+ <th className="p-2">KEY</th>
456
+ <th className="p-2">AGENT</th>
457
+ <th className="p-2">PERMISSIONS</th>
458
+ <th className="p-2">ISSUED</th>
459
+ <th className="p-2">EXPIRES</th>
460
+ <th className="p-2">LAST USED</th>
461
+ <th className="p-2">ACTIONS</th>
462
+ </tr>
463
+ </thead>
464
+ <tbody>
465
+ {filteredTokenRows.map((row) => {
466
+ const lastUsed = getLastUsed(row);
467
+ const isRevoking = revokingTokenKey === row.key;
468
+ const canRevoke = row.status === 'active' && Boolean(row.token.tokenHash);
469
+ return (
470
+ <tr key={row.key} className="border-b font-mono text-[9px]">
471
+ <td className="p-2">
472
+ <div>{shortHash(row.token.tokenHash ?? row.key)}</div>
473
+ <div className={`text-[8px] uppercase ${statusTone(row.status)}`}>{row.status}</div>
474
+ </td>
475
+ <td className="p-2">{row.token.agentId ?? 'unknown'}</td>
476
+ <td className="p-2">{permissionsSummary(row.token.permissions ?? [])}</td>
477
+ <td className="p-2">{formatDate(row.token.createdAt)}</td>
478
+ <td className="p-2">
479
+ {formatDate(row.token.expiresAt)}
480
+ <div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">{formatRelative(row.token.expiresAt)}</div>
481
+ </td>
482
+ <td className="p-2">{lastUsed ? new Date(lastUsed).toLocaleString() : '—'}</td>
483
+ <td className="p-2">
484
+ <div className="flex items-center gap-1">
485
+ <button
486
+ onClick={() => void handleCopyShortHash(row.key, row.token.tokenHash)}
487
+ className="border px-1.5 py-0.5 text-[8px] disabled:opacity-40"
488
+ title="Copy short hash"
489
+ disabled={!row.token.tokenHash}
490
+ >
491
+ <span className="inline-flex items-center gap-1">
492
+ <Copy size={8} />
493
+ {copiedTokenKey === row.key ? 'COPIED' : 'COPY'}
494
+ </span>
495
+ </button>
496
+ <button
497
+ onClick={() => handleOpenActivity(row.key)}
498
+ className="border px-1.5 py-0.5 text-[8px]"
499
+ title="Open activity"
500
+ >
501
+ VIEW
502
+ </button>
503
+ <button
504
+ onClick={() => void handleRevoke(row.key, row.token.tokenHash)}
505
+ disabled={!canRevoke || isRevoking}
506
+ className="border px-1.5 py-0.5 text-[8px] disabled:opacity-40 text-[var(--color-warning,#ff4d00)]"
507
+ title={canRevoke ? 'Revoke token' : 'Token cannot be revoked'}
508
+ >
509
+ {isRevoking ? 'REVOKING' : 'REVOKE'}
510
+ </button>
511
+ </div>
512
+ </td>
513
+ </tr>
514
+ );
515
+ })}
516
+ </tbody>
517
+ </table>
518
+ )}
519
+ </div>
520
+ </div>
521
+
522
+ <div ref={activitySectionRef} className="flex-1 overflow-auto border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)]">
523
+ <div className="sticky top-0 z-10 px-2 py-1.5 border-b bg-[var(--color-surface-alt,#fafafa)] font-mono text-[9px] flex items-center justify-between">
524
+ <span>TOKEN ACTIVITY · {filtered.length} ROWS</span>
525
+ <span className="text-[var(--color-text-muted,#6b7280)]">
526
+ PAGE {page + 1} / {Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))}
527
+ </span>
528
+ </div>
529
+
530
+ {events.length >= HARD_CAP && (
531
+ <div className="font-mono text-[9px] border-b border-[var(--color-warning,#ff4d00)] p-2 text-[var(--color-warning,#ff4d00)]">
532
+ RESULTS TRUNCATED AT 500 ROWS — NARROW FILTERS TO REDUCE VOLUME.
533
+ </div>
534
+ )}
535
+
536
+ {pagedRows.length === 0 ? (
537
+ <div className="p-4 font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">NO AUDIT EVENTS IN WINDOW</div>
538
+ ) : (
539
+ <table className="w-full text-left">
540
+ <thead className="sticky top-[31px] bg-[var(--color-surface-alt,#fafafa)] border-b">
541
+ <tr className="font-mono text-[9px]">
542
+ <th className="p-2">TIME</th>
543
+ <th className="p-2">TOKEN / AGENT</th>
544
+ <th className="p-2">DECISION</th>
545
+ <th className="p-2">REASON</th>
546
+ <th className="p-2">CONFIDENCE</th>
547
+ <th className="p-2">ENDPOINT</th>
548
+ </tr>
549
+ </thead>
550
+ <tbody>
551
+ {pagedRows.map((row) => (
552
+ <tr key={row.id} className="border-b font-mono text-[9px]">
553
+ <td className="p-2">{new Date(row.timestamp).toLocaleString()}</td>
554
+ <td className="p-2">{row.agentId ?? 'unknown'} · {row.tokenKey.slice(0, 10)}</td>
555
+ <td className="p-2">{row.decision}</td>
556
+ <td className="p-2">{row.reasonCode}{row.rawReasonCode && row.reasonCode === 'UNKNOWN' ? ` (${row.rawReasonCode})` : ''}</td>
557
+ <td className="p-2" title={row.confidence === 'HIGH' ? 'Direct token hash match' : row.confidence === 'MEDIUM' ? 'Agent correlation' : 'Partial/ambiguous join'}>{row.confidence}</td>
558
+ <td className="p-2">{row.endpoint ?? '—'}</td>
559
+ </tr>
560
+ ))}
561
+ </tbody>
562
+ </table>
563
+ )}
564
+ </div>
565
+
566
+ <div className="flex items-center justify-end gap-2">
567
+ <button
568
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
569
+ disabled={page === 0}
570
+ className="font-mono text-[9px] border px-2 py-1 disabled:opacity-40"
571
+ >
572
+ PREV
573
+ </button>
574
+ <button
575
+ onClick={() => setPage((p) => (p + 1) * PAGE_SIZE < filtered.length ? p + 1 : p)}
576
+ disabled={(page + 1) * PAGE_SIZE >= filtered.length}
577
+ className="font-mono text-[9px] border px-2 py-1 disabled:opacity-40"
578
+ >
579
+ NEXT
580
+ </button>
581
+ </div>
582
+ </div>
583
+ );
584
+ }