failproofai 1.0.7 → 1.0.8

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 (268) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -0,0 +1,630 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The Jev panel on /settings — the dashboard's half of `failproofai jev setup`.
5
+ *
6
+ * ## What it is for
7
+ *
8
+ * Jev is the optional second evaluation tier. It is on when, and only when, a
9
+ * valid `~/.failproofai/jev.json` exists; without it the hook path is byte for
10
+ * byte what it was before. So this panel has two jobs, in this order:
11
+ *
12
+ * 1. say whether it is on, in which mode, against which provider and model,
13
+ * how much of the enabled policy set it is allowed to clear, and — once it
14
+ * has run — how often it fell back to the regex engine. A panel that only
15
+ * took input would leave "is this thing working" unanswerable from the
16
+ * dashboard.
17
+ * 2. take the endpoint and the token.
18
+ *
19
+ * Each fact is stated ONCE. The status block says nothing the form below it
20
+ * already holds: the endpoint and the account id are form values, and printing
21
+ * them above the fields put a per-account URL — and the id inside it — twice on
22
+ * one screen. The config file's path and permission bits are gone for a
23
+ * different reason: nobody repairs a 0644 from a browser, and the loader's own
24
+ * refusal already names the file when it matters.
25
+ *
26
+ * It is a full-width cell in the same hairline console as the scheduled-audit
27
+ * panel, using the same tokens, the same `.btn-press` action and the same
28
+ * `toast()` on success. No new colour and no new control that did not already
29
+ * exist on this page, except a `<select>` — five providers do not fit a switch.
30
+ *
31
+ * ## The token is write-only
32
+ *
33
+ * The field is always blank on load, whatever is stored. `JevSettingsView`
34
+ * carries presence and nothing else, so the panel can say "configured" — or
35
+ * that the key is read from the environment — and no more; the value itself
36
+ * never leaves the machine's filesystem, and neither does a fragment of it.
37
+ * Leaving the field empty on save KEEPS the key where it is — the stored one, or
38
+ * the environment's for a config that takes it from there — which the server
39
+ * decides, not this component.
40
+ *
41
+ * The stored MODEL is shown the same way when it does not look like a model id,
42
+ * for the same reason: it is the one routing field someone can paste a key into.
43
+ * The panel gets a `JevModelView`, never the string, so there is nothing here to
44
+ * leak even by accident. It is display-only — the form does not offer the field,
45
+ * and a save leaves whatever is stored alone.
46
+ *
47
+ * Client-side validation here is a convenience only. Every rule — the URL
48
+ * scheme, plain http being refused outside shadow mode, cloudflare's account id
49
+ * — is enforced server-side by the same `validateJevConfig` the loader runs.
50
+ *
51
+ * ## FailproofAI Cloud
52
+ *
53
+ * When `jev.json` names the FailproofAI Cloud provider, the endpoint and the
54
+ * key are not this page's to edit: both come from the machine's connection
55
+ * (`config --token`). So the form is replaced by the two controls that are
56
+ * the owner's — an on/off switch and shadow/enforce — both through
57
+ * `setJevModeAction`, which rewrites `mode` and nothing else. "Off" there keeps
58
+ * the file (`mode: "off"`): deleting it would leave nothing on this page to
59
+ * switch back on — the only way to get the file back would be re-running
60
+ * `config --token` with the key.
61
+ *
62
+ * The connection itself — org, and whether the key carries Jev — is one row,
63
+ * read from `credentials.json` by the server with no network call; the key is
64
+ * never part of what this component receives.
65
+ */
66
+
67
+ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
68
+ import {
69
+ getJevSettingsAction,
70
+ type JevModelView,
71
+ type JevSettingsView,
72
+ } from "@/app/actions/get-jev-config";
73
+ import {
74
+ removeJevConfigAction,
75
+ saveJevConfigAction,
76
+ setJevModeAction,
77
+ } from "@/app/actions/update-jev-config";
78
+ import { toast } from "@/app/components/toast";
79
+
80
+ const PROVIDERS = [
81
+ { value: "typesafe", label: "typesafe" },
82
+ { value: "openrouter", label: "openrouter" },
83
+ { value: "vercel", label: "vercel ai gateway" },
84
+ { value: "cloudflare", label: "cloudflare workers ai" },
85
+ { value: "custom", label: "custom endpoint" },
86
+ ] as const;
87
+
88
+ const MODES = [
89
+ { value: "enforce", label: "enforce — jev's verdict counts" },
90
+ { value: "shadow", label: "shadow — log only, regex decides" },
91
+ { value: "off", label: "off — keep this config, don't ask jev" },
92
+ ] as const;
93
+
94
+ /** The provider whose endpoint and key come from the FailproofAI Cloud connection. */
95
+ const CLOUD_PROVIDER = "failproofai";
96
+
97
+ /** What a provider is called on this page. */
98
+ function providerName(provider: string | null): string {
99
+ return provider === CLOUD_PROVIDER ? "FailproofAI Cloud" : (provider ?? "");
100
+ }
101
+
102
+ /** The connection row: org, and whether the key carries Jev. No key, ever. */
103
+ function fmtConnection(cloud: JevSettingsView["cloud"] | undefined): string {
104
+ if (!cloud || !cloud.connected) {
105
+ return cloud?.jev === "yes" ? "a jev key is stored, but no connection is" : "not connected";
106
+ }
107
+ const where = cloud.org ?? cloud.host ?? "connected";
108
+ const jev =
109
+ cloud.jev === "yes"
110
+ ? "key carries jev"
111
+ : cloud.jev === "refused"
112
+ ? "jev key refused — credentials.json is not owner-only"
113
+ : "no jev key stored";
114
+ return `connected to ${where} · ${jev}`;
115
+ }
116
+
117
+ /** "24h" / "7d" / "30m", for the stats window. */
118
+ function fmtWindow(ms: number): string {
119
+ const h = ms / 3_600_000;
120
+ if (Number.isInteger(h) && h >= 1) return h % 24 === 0 ? `${h / 24}d` : `${h}h`;
121
+ return `${Math.max(1, Math.round(ms / 60_000))}m`;
122
+ }
123
+
124
+ /** A rate as a whole percent. 0.0417 → "4%". */
125
+ function fmtRate(rate: number): string {
126
+ return `${Math.round(rate * 100)}%`;
127
+ }
128
+
129
+ /**
130
+ * The model row. `withheld` is the case worth having a row for at all: the file
131
+ * holds something in that slot that does not look like a model id, which is
132
+ * what a key pasted one field off looks like, so it is described and not
133
+ * printed — and the server never sent it here to print. The loader's own reason
134
+ * and its fix are already on the page above, in `view.problem`.
135
+ */
136
+ function fmtModel(model: JevModelView): string {
137
+ switch (model.kind) {
138
+ case "id":
139
+ return model.id;
140
+ case "withheld":
141
+ return "set to something that is not a model id — not shown here, in case it is a key";
142
+ default:
143
+ return "the provider's default";
144
+ }
145
+ }
146
+
147
+ interface FormState {
148
+ provider: string;
149
+ baseUrl: string;
150
+ accountId: string;
151
+ mode: string;
152
+ }
153
+
154
+ function formFrom(view: JevSettingsView): FormState {
155
+ return {
156
+ // The form is the BYOK form: the Cloud provider is never one of its values.
157
+ provider: view.provider && view.provider !== CLOUD_PROVIDER ? view.provider : "typesafe",
158
+ baseUrl: view.baseUrl,
159
+ accountId: view.accountId,
160
+ mode: view.mode,
161
+ };
162
+ }
163
+
164
+ /** One labelled control, laid out like the interval row above it. */
165
+ function Field({
166
+ id,
167
+ label,
168
+ hint,
169
+ children,
170
+ }: {
171
+ id: string;
172
+ label: string;
173
+ hint?: string;
174
+ children: ReactNode;
175
+ }) {
176
+ return (
177
+ <div className="set-field">
178
+ <label className="set-field-label" htmlFor={id}>
179
+ {label}
180
+ </label>
181
+ {children}
182
+ {hint ? <span className="set-hint">{hint}</span> : null}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export default function JevPanel({ initial }: { initial: JevSettingsView | null }) {
188
+ const [view, setView] = useState<JevSettingsView | null>(initial);
189
+ const [form, setForm] = useState<FormState>(
190
+ initial ? formFrom(initial) : { provider: "typesafe", baseUrl: "", accountId: "", mode: "enforce" },
191
+ );
192
+ const [token, setToken] = useState("");
193
+ const [busy, setBusy] = useState(false);
194
+ const [problem, setProblem] = useState<string | null>(null);
195
+ /**
196
+ * The server's `needsToken`: the one refusal whose remedy is this field, so it
197
+ * is marked rather than leaving the person to match the message to a control.
198
+ * The refusals that no token can settle deliberately do not set it.
199
+ */
200
+ const [needsToken, setNeedsToken] = useState(false);
201
+ const mounted = useRef(true);
202
+ /**
203
+ * Whether the user has touched the form since the last read. A focus refresh
204
+ * must not overwrite a half-typed endpoint with what is still on disk — the
205
+ * page re-reads on every `visibilitychange`, which includes the tab hide that
206
+ * happens when somebody alt-tabs to their password manager mid-edit.
207
+ */
208
+ const dirty = useRef(false);
209
+
210
+ const reload = useCallback(async () => {
211
+ try {
212
+ const next = await getJevSettingsAction();
213
+ if (!mounted.current) return;
214
+ setView(next);
215
+ if (!dirty.current) setForm(formFrom(next));
216
+ } catch {
217
+ // Leave what is on screen: it describes real machine state, and blanking
218
+ // it would report something less true than what is already there.
219
+ }
220
+ }, []);
221
+
222
+ useEffect(() => {
223
+ mounted.current = true;
224
+ void reload();
225
+ return () => {
226
+ mounted.current = false;
227
+ };
228
+ }, [reload]);
229
+
230
+ useEffect(() => {
231
+ // Same reason as the scheduled-audit panel: `failproofai jev setup` in a
232
+ // terminal changes the same file, and a page left open would otherwise keep
233
+ // showing the old state until it was reloaded by hand.
234
+ const onFocus = () => void reload();
235
+ window.addEventListener("focus", onFocus);
236
+ document.addEventListener("visibilitychange", onFocus);
237
+ return () => {
238
+ window.removeEventListener("focus", onFocus);
239
+ document.removeEventListener("visibilitychange", onFocus);
240
+ };
241
+ }, [reload]);
242
+
243
+ const set = useCallback(<K extends keyof FormState>(key: K, value: FormState[K]) => {
244
+ dirty.current = true;
245
+ setForm((f) => ({ ...f, [key]: value }));
246
+ }, []);
247
+
248
+ const onSave = useCallback(async () => {
249
+ setBusy(true);
250
+ setProblem(null);
251
+ try {
252
+ // The form does not offer a model, so it says nothing about one: the
253
+ // server keeps a stored model when the provider is unchanged, and drops it
254
+ // for the new provider's default when it is not — the same rule `jev setup`
255
+ // applies. `JevConfigInput` has no model field for this to get wrong.
256
+ const res = await saveJevConfigAction({
257
+ provider: form.provider,
258
+ baseUrl: form.baseUrl,
259
+ accountId: form.accountId,
260
+ mode: form.mode,
261
+ token,
262
+ });
263
+ if (!res.ok) {
264
+ setProblem(res.problem);
265
+ setNeedsToken(res.needsToken === true);
266
+ return;
267
+ }
268
+ dirty.current = false;
269
+ setNeedsToken(false);
270
+ setToken("");
271
+ setView(res.view);
272
+ setForm(formFrom(res.view));
273
+ toast(res.view.on ? "jev is on." : "saved.");
274
+ } catch {
275
+ setProblem("could not save that.");
276
+ } finally {
277
+ setBusy(false);
278
+ }
279
+ }, [form, token]);
280
+
281
+ /**
282
+ * The FailproofAI Cloud switch: `off`, `shadow` or `enforce`, and nothing
283
+ * else about the file changes (see `setJevModeAction`).
284
+ */
285
+ const onMode = useCallback(async (mode: "off" | "shadow" | "enforce") => {
286
+ setBusy(true);
287
+ setProblem(null);
288
+ try {
289
+ const res = await setJevModeAction(mode);
290
+ if (!res.ok) {
291
+ setProblem(res.problem);
292
+ return;
293
+ }
294
+ dirty.current = false;
295
+ setView(res.view);
296
+ setForm(formFrom(res.view));
297
+ toast(mode === "off" ? "jev is off. hooks run the regex policies." : `jev is on, in ${mode} mode.`);
298
+ } catch {
299
+ setProblem("could not switch that.");
300
+ } finally {
301
+ setBusy(false);
302
+ }
303
+ }, []);
304
+
305
+ const onRemove = useCallback(async () => {
306
+ setBusy(true);
307
+ setProblem(null);
308
+ setNeedsToken(false);
309
+ try {
310
+ const res = await removeJevConfigAction();
311
+ if (!res.ok) {
312
+ setProblem(res.problem);
313
+ return;
314
+ }
315
+ dirty.current = false;
316
+ setToken("");
317
+ setView(res.view);
318
+ setForm(formFrom(res.view));
319
+ toast("jev is off. hooks run the regex policies.");
320
+ } catch {
321
+ setProblem("could not turn that off.");
322
+ } finally {
323
+ setBusy(false);
324
+ }
325
+ }, []);
326
+
327
+ const configured = view !== null && view.status !== "absent";
328
+ const cloudRoute = view?.provider === CLOUD_PROVIDER;
329
+ const stored = view?.token;
330
+ const tokenHint = needsToken
331
+ ? "needed for this save — type the token for this endpoint"
332
+ : stored
333
+ ? stored.source === "cloud"
334
+ ? "from this machine's FailproofAI Cloud connection"
335
+ : stored.source === "env"
336
+ ? "configured, read from the environment — leave blank to keep it"
337
+ : // Presence and the keep rule, which is everything this field's reader
338
+ // has to decide. Naming the last four characters of the stored key
339
+ // here put that fragment on the page a second time, next to the input
340
+ // it would be re-typed into.
341
+ "configured — leave blank to keep it"
342
+ : // The file names the environment as the key's source and the variable is
343
+ // unset in this process. Leaving the field blank keeps that arrangement,
344
+ // which is not the same thing as "there is no key" — the endpoint and the
345
+ // mode are editable from here without one.
346
+ view?.status === "key-missing"
347
+ ? "read from the environment, which is not set in this shell — leave blank to keep it that way"
348
+ : "sent as a bearer token; stored owner-only at 0600";
349
+
350
+ return (
351
+ <div className="set-cell set-jev">
352
+ <h2 className="set-cell-title">jev · semantic evaluation</h2>
353
+
354
+ <div className="set-row set-row-main">
355
+ <div className="set-row-copy">
356
+ <span className={view?.on ? "set-strong" : "set-dim"}>
357
+ {view === null
358
+ ? "reading…"
359
+ : view.on
360
+ ? `on. every judged command is also asked of ${providerName(view.provider)}.`
361
+ : view.status === "off"
362
+ ? "off — switched off. hooks run the regex policies exactly as before."
363
+ : view.status === "not-connected"
364
+ ? "off — this machine is not connected to FailproofAI Cloud. hooks run the regex policies."
365
+ : view.status === "key-lacks-jev"
366
+ ? "off — no jev key is stored for this machine's FailproofAI Cloud connection. hooks run the regex policies."
367
+ : view.status === "key-missing"
368
+ ? // The CLI's "off in this shell": the key is read by whatever
369
+ // runs the hook, and this server's environment is not that.
370
+ "off for this dashboard — FAILPROOFAI_JEV_API_KEY is not set in its environment. hooks run the regex policies wherever it is unset, including under the daemon; a session that sets it still consults jev."
371
+ : "off. hooks run the regex policies exactly as before."}
372
+ </span>
373
+ <span className="set-dim">
374
+ {view?.on
375
+ ? view.mode === "shadow"
376
+ ? "shadow: jev's answers are logged, the regex result is what gets enforced."
377
+ : "enforce: jev's answers can clear a reviewable deny."
378
+ : cloudRoute
379
+ ? "through FailproofAI Cloud, on your org's plan — no endpoint or token of your own."
380
+ : "a second opinion on the commands the regex policies flag. needs your own endpoint and token."}
381
+ </span>
382
+ </div>
383
+ </div>
384
+
385
+ {/* Where requests GO is not in this block, deliberately. The editable
386
+ field below holds it, and for Cloudflare that URL is
387
+ `/accounts/<id>/ai/run` — so a read-only row above the form printed the
388
+ same per-account address, and the account id inside it, a second time
389
+ on one screen. The path of the config file and its mode were here too;
390
+ nobody acts on either from a browser. */}
391
+ {/* The FailproofAI Cloud connection, on every state: whether this
392
+ machine could run Jev through the Cloud is worth knowing before it
393
+ does. Org and a yes/no, never a key. */}
394
+ {view && (
395
+ <dl className="set-how-list set-jev-cloud">
396
+ <div className="set-how-row">
397
+ <dt className="set-how-label">FailproofAI Cloud connection</dt>
398
+ <dd className="set-how-body">{fmtConnection(view.cloud)}</dd>
399
+ </div>
400
+ </dl>
401
+ )}
402
+
403
+ {view && configured && (
404
+ <dl className="set-how-list">
405
+ {cloudRoute && (
406
+ <div className="set-how-row">
407
+ <dt className="set-how-label">provider</dt>
408
+ <dd className="set-how-body">
409
+ {view.cloud?.host ? `FailproofAI Cloud · ${view.cloud.host}` : "FailproofAI Cloud"}
410
+ </dd>
411
+ </div>
412
+ )}
413
+ <div className="set-how-row">
414
+ <dt className="set-how-label">model</dt>
415
+ <dd className="set-how-body">{fmtModel(view.model)}</dd>
416
+ </div>
417
+ {/* Presence, and where it came from. Not four characters of it: that
418
+ is a recognisable fragment of a live credential on a page with no
419
+ authentication, and it tells the reader nothing they cannot get by
420
+ re-pasting the key. The server no longer sends it either. */}
421
+ <div className="set-how-row">
422
+ <dt className="set-how-label">token</dt>
423
+ <dd className="set-how-body">
424
+ {stored
425
+ ? stored.source === "cloud"
426
+ ? "FailproofAI Cloud connection"
427
+ : stored.source === "env"
428
+ ? "from FAILPROOFAI_JEV_API_KEY"
429
+ : "configured"
430
+ : cloudRoute
431
+ ? view.cloud?.connected
432
+ ? "none stored for this connection"
433
+ : "none — this machine is not connected"
434
+ : "none stored"}
435
+ </dd>
436
+ </div>
437
+ {view.reviewable && (
438
+ <div className="set-how-row">
439
+ <dt className="set-how-label">reviewable</dt>
440
+ {/* The count, in the server's words — the same sentence
441
+ `failproofai jev status` prints, because the two surfaces must
442
+ not disagree about what Jev is allowed to clear. */}
443
+ <dd className="set-how-body">{view.reviewable.summary}</dd>
444
+ </div>
445
+ )}
446
+ {view.stats && view.stats.total > 0 && (
447
+ <div className="set-how-row">
448
+ <dt className="set-how-label">fallbacks</dt>
449
+ {/* One string, not an interpolated fragment: a sentence split
450
+ across text nodes is a sentence a screen reader and a test both
451
+ have to reassemble. */}
452
+ <dd className="set-how-body">
453
+ {`${fmtRate(view.stats.fallbackRate)} of ${view.stats.total} calls in the last ${fmtWindow(view.stats.windowMs)} fell back to the regex engine.`}
454
+ </dd>
455
+ </div>
456
+ )}
457
+ </dl>
458
+ )}
459
+
460
+ {view?.problem && (
461
+ <p className="set-warn">
462
+ {view.problem}
463
+ {view.fix ? ` — ${view.fix}` : ""}
464
+ </p>
465
+ )}
466
+
467
+ {/* Jev is on, answering, and cannot clear a single verdict. Nothing else
468
+ on this page shows it: a policy set with no authority marks behaves
469
+ exactly like a healthy one until you notice that nothing is ever
470
+ cleared. The server sends the cause and the remedy together. */}
471
+ {view?.reviewable?.problem && <p className="set-warn">{view.reviewable.problem}</p>}
472
+
473
+ <div className="set-rule" />
474
+
475
+ {cloudRoute ? (
476
+ <>
477
+ <div className="set-jev-fields">
478
+ <Field
479
+ id="jev-cloud-mode"
480
+ label="mode"
481
+ hint="the endpoint and key come from this machine's FailproofAI Cloud connection"
482
+ >
483
+ <select
484
+ id="jev-cloud-mode"
485
+ className="set-select"
486
+ value={view?.mode === "enforce" ? "enforce" : "shadow"}
487
+ disabled={busy || view?.mode === "off"}
488
+ onChange={(e) => void onMode(e.target.value === "enforce" ? "enforce" : "shadow")}
489
+ >
490
+ {MODES.filter((m) => m.value !== "off").map((m) => (
491
+ <option key={m.value} value={m.value}>
492
+ {m.label}
493
+ </option>
494
+ ))}
495
+ </select>
496
+ </Field>
497
+ </div>
498
+
499
+ {problem && <p className="set-warn">{problem}</p>}
500
+
501
+ <div className="set-actions set-jev-actions">
502
+ <button
503
+ type="button"
504
+ className="btn btn-press"
505
+ disabled={busy}
506
+ onClick={() => void onMode(view?.mode === "off" ? "shadow" : "off")}
507
+ >
508
+ {busy ? "[ switching… ]" : view?.mode === "off" ? "[ turn jev on ]" : "[ turn jev off ]"}
509
+ </button>
510
+ </div>
511
+ </>
512
+ ) : (
513
+ <>
514
+ <div className="set-jev-fields">
515
+ <Field id="jev-provider" label="provider">
516
+ <select
517
+ id="jev-provider"
518
+ className="set-select"
519
+ value={form.provider}
520
+ disabled={busy}
521
+ onChange={(e) => set("provider", e.target.value)}
522
+ >
523
+ {PROVIDERS.map((p) => (
524
+ <option key={p.value} value={p.value}>
525
+ {p.label}
526
+ </option>
527
+ ))}
528
+ </select>
529
+ </Field>
530
+
531
+ <Field
532
+ id="jev-url"
533
+ label="endpoint url"
534
+ hint={[
535
+ form.provider === "custom"
536
+ ? "required — https, or http to localhost in shadow mode"
537
+ : "leave blank for the provider's own api",
538
+ // The stored query string is not sent to this page (see
539
+ // `baseUrlView` in `get-jev-config.ts`), so the field holds the URL
540
+ // without it. Said out loud, because the alternative is a field that
541
+ // silently disagrees with the file and a save that looks like it
542
+ // kept something the person was never shown.
543
+ view?.baseUrlQueryWithheld
544
+ ? "the stored url's query string is not shown here — leave the url as it is to keep it"
545
+ : null,
546
+ ]
547
+ .filter(Boolean)
548
+ .join(" · ")}
549
+ >
550
+ <input
551
+ id="jev-url"
552
+ type="text"
553
+ className="set-text"
554
+ inputMode="url"
555
+ autoComplete="off"
556
+ spellCheck={false}
557
+ placeholder={form.provider === "custom" ? "https://…" : "the provider's own api"}
558
+ value={form.baseUrl}
559
+ disabled={busy}
560
+ onChange={(e) => set("baseUrl", e.target.value)}
561
+ />
562
+ </Field>
563
+
564
+ {form.provider === "cloudflare" && (
565
+ <Field id="jev-account" label="account id" hint="32 lowercase hex characters">
566
+ <input
567
+ id="jev-account"
568
+ type="text"
569
+ className="set-text"
570
+ autoComplete="off"
571
+ spellCheck={false}
572
+ value={form.accountId}
573
+ disabled={busy}
574
+ onChange={(e) => set("accountId", e.target.value)}
575
+ />
576
+ </Field>
577
+ )}
578
+
579
+ <Field id="jev-token" label="token" hint={tokenHint}>
580
+ <input
581
+ id="jev-token"
582
+ type="password"
583
+ className="set-text"
584
+ autoComplete="off"
585
+ spellCheck={false}
586
+ placeholder={stored ? "•••• kept" : ""}
587
+ value={token}
588
+ disabled={busy}
589
+ aria-invalid={needsToken || undefined}
590
+ onChange={(e) => {
591
+ dirty.current = true;
592
+ setToken(e.target.value);
593
+ }}
594
+ />
595
+ </Field>
596
+
597
+ <Field id="jev-mode" label="mode">
598
+ <select
599
+ id="jev-mode"
600
+ className="set-select"
601
+ value={form.mode}
602
+ disabled={busy}
603
+ onChange={(e) => set("mode", e.target.value)}
604
+ >
605
+ {MODES.map((m) => (
606
+ <option key={m.value} value={m.value}>
607
+ {m.label}
608
+ </option>
609
+ ))}
610
+ </select>
611
+ </Field>
612
+ </div>
613
+
614
+ {problem && <p className="set-warn">{problem}</p>}
615
+
616
+ <div className="set-actions set-jev-actions">
617
+ <button type="button" className="btn btn-press" disabled={busy} onClick={() => void onSave()}>
618
+ {busy ? "[ saving… ]" : configured ? "[ save changes ]" : "[ turn jev on ]"}
619
+ </button>
620
+ {configured && (
621
+ <button type="button" className="set-link" disabled={busy} onClick={() => void onRemove()}>
622
+ turn jev off
623
+ </button>
624
+ )}
625
+ </div>
626
+ </>
627
+ )}
628
+ </div>
629
+ );
630
+ }
@@ -4,6 +4,11 @@ import {
4
4
  getScheduledAuditAction,
5
5
  type ScheduledAuditView,
6
6
  } from "@/app/actions/get-scheduled-audit";
7
+ import {
8
+ getJevSettingsAction,
9
+ type JevSettingsView,
10
+ } from "@/app/actions/get-jev-config";
11
+ import JevPanel from "./jev-panel";
7
12
  import SettingsClient from "./settings-client";
8
13
 
9
14
  export const metadata: Metadata = {
@@ -44,5 +49,19 @@ export default async function SettingsPage() {
44
49
  // here would replace a page that can explain itself with an error boundary
45
50
  // that cannot.
46
51
  }
47
- return <SettingsClient initial={initial} />;
52
+ // Same reasoning as above, for the same reason it matters more here: this
53
+ // panel's headline fact is whether a second evaluator is switched on, and a
54
+ // client-side load would paint "off" first and correct itself after.
55
+ // `getJevSettingsAction` does not throw, but a failed read must not take the
56
+ // scheduled-audit panel down with it either.
57
+ let jev: JevSettingsView | null = null;
58
+ try {
59
+ jev = await getJevSettingsAction();
60
+ } catch {
61
+ // Left null; the panel reads as "reading…" until the client load lands.
62
+ }
63
+ // The panel is composed in rather than imported by `SettingsClient`, so the
64
+ // scheduled-audit console and the Jev console share a layout without sharing
65
+ // a dependency — see the `jevPanel` prop.
66
+ return <SettingsClient initial={initial} jevPanel={<JevPanel initial={jev} />} />;
48
67
  }