failproofai 1.0.6 → 1.0.7-beta.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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +7 -5
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +13 -11
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +7 -6
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +41 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__1j-zsg5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_r2rbg._.js → [root-of-the-server]__1bf34x4._.js} +5 -7
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b2dbet._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1svlq8e._.js → [root-of-the-server]__1p28m0u._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_17o744m._.js → _0cf1olg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0do6d26._.js → _135-c3y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _1w_5l7t._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_0az0sfq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +56 -23
- package/.next/standalone/.next/static/chunks/{25m8mg7bosgip.js → 00i9thnpht4l1.js} +1 -1
- package/.next/standalone/.next/static/chunks/0289oapk24zql.js +1 -0
- package/.next/standalone/.next/static/chunks/{35pw7zxhh76n7.js → 0pyy9m5cm8zku.js} +1 -1
- package/.next/standalone/.next/static/chunks/15heikmy3yjzc.js +1 -0
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +2 -0
- package/.next/standalone/.next/static/chunks/{40-wncjnguy4e.js → 2plg9qae1yejv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/2tqszh84mp_2l.js +1 -0
- package/.next/standalone/.next/static/chunks/34jpdb2xjm-x7.js +1 -0
- package/.next/standalone/.next/static/chunks/3cdkxrukvlvul.js +1 -0
- package/.next/standalone/.next/static/chunks/3dnd-_29uiaj3.js +6 -0
- package/.next/standalone/.next/static/chunks/3vp7c3knkh8mr.js +1 -0
- package/.next/standalone/.next/static/chunks/{2y4ycwcwfjv4k.js → 3vwnzfx9q80yl.js} +1 -1
- package/.next/standalone/app/actions/get-jev-config.ts +409 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +420 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +9 -0
- package/.next/standalone/app/settings/jev-panel.tsx +469 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +51 -3
- package/.next/standalone/fp-cloud-cli/README.md +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +44 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +226 -5
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +11 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +68 -1
- package/.next/standalone/fp-cloud-cli/pyproject.toml +1 -1
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +17 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +7 -4
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +184 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
- package/.next/standalone/package.json +11 -11
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +2 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +24 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +41 -6
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +88 -0
- package/.next/standalone/sdk/python/uv.lock +15 -15
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +115 -2
- package/dist/cli.mjs +9283 -2149
- package/dist/index.js +19 -1
- package/dist/worker.mjs +13761 -7834
- package/package.json +11 -11
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +39 -2
- package/src/hooks/builtin-policies.ts +21 -3
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/custom-hooks-loader.ts +45 -7
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +79 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/handler.ts +265 -6
- package/src/hooks/hook-activity-store.ts +105 -1
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +1193 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/pack-cli.ts +237 -11
- package/src/hooks/pack-manifest.ts +479 -7
- package/src/hooks/pack-store.ts +156 -10
- package/src/hooks/policy-authority.ts +333 -0
- package/src/hooks/policy-catalog.ts +65 -0
- package/src/hooks/policy-evaluator.ts +940 -796
- package/src/hooks/policy-registry.ts +25 -0
- package/src/hooks/policy-reviewability.ts +229 -0
- package/src/hooks/policy-types.ts +126 -0
- package/src/hooks/semantic/combine.ts +541 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +392 -0
- package/src/hooks/semantic/envelope.ts +1296 -0
- package/src/hooks/semantic/evaluator.ts +547 -0
- package/src/hooks/semantic/facts.ts +292 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +643 -0
- package/src/hooks/semantic/jev-config.ts +594 -0
- package/src/hooks/semantic/jev-review.ts +374 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +421 -0
- package/src/hooks/semantic/pack-policies.ts +251 -0
- package/src/hooks/semantic/policies.ts +596 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2910 -0
- package/src/hooks/semantic/types.ts +145 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/worker-server.ts +119 -26
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +0 -10
- package/.next/standalone/.next/static/chunks/0bepqeawy72n6.js +0 -1
- package/.next/standalone/.next/static/chunks/0h-gz8e00478q.js +0 -1
- package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +0 -2
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2lvr2pt70qn14.js +0 -1
- package/.next/standalone/.next/static/chunks/3-6zhytp6adkk.js +0 -1
- package/.next/standalone/.next/static/chunks/319c_91le7-h_.js +0 -1
- package/.next/standalone/.next/static/chunks/35m53kix7uaii.js +0 -6
- package/.next/standalone/.next/static/chunks/3xacd7qmuqlvs.js +0 -1
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → qbVI48kgBOf9CTOKcFIk2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → qbVI48kgBOf9CTOKcFIk2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → qbVI48kgBOf9CTOKcFIk2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,469 @@
|
|
|
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, where it sends requests, in which mode, how much
|
|
13
|
+
* of the enabled policy set it is allowed to clear, and — once it has run
|
|
14
|
+
* — how often it fell back to the regex engine. A panel that only took
|
|
15
|
+
* input would leave "is this thing working" unanswerable from the
|
|
16
|
+
* dashboard.
|
|
17
|
+
* 2. take the endpoint and the token.
|
|
18
|
+
*
|
|
19
|
+
* It is a full-width cell in the same hairline console as the scheduled-audit
|
|
20
|
+
* panel, using the same tokens, the same `.btn-press` action and the same
|
|
21
|
+
* `toast()` on success. No new colour and no new control that did not already
|
|
22
|
+
* exist on this page, except a `<select>` — five providers do not fit a switch.
|
|
23
|
+
*
|
|
24
|
+
* ## The token is write-only
|
|
25
|
+
*
|
|
26
|
+
* The field is always blank on load, whatever is stored. `JevSettingsView`
|
|
27
|
+
* carries a presence flag and at most the last four characters, so the panel
|
|
28
|
+
* can say "configured, ending 3f2a" and nothing more; the value itself never
|
|
29
|
+
* leaves the machine's filesystem. Leaving the field empty on save KEEPS the
|
|
30
|
+
* key where it is — the stored one, or the environment's for a config that takes
|
|
31
|
+
* it from there — which the server decides, not this component.
|
|
32
|
+
*
|
|
33
|
+
* The stored MODEL is shown the same way when it does not look like a model id,
|
|
34
|
+
* for the same reason: it is the one routing field someone can paste a key into.
|
|
35
|
+
* The panel gets a `JevModelView`, never the string, so there is nothing here to
|
|
36
|
+
* leak even by accident. It is display-only — the form does not offer the field,
|
|
37
|
+
* and a save leaves whatever is stored alone.
|
|
38
|
+
*
|
|
39
|
+
* Client-side validation here is a convenience only. Every rule — the URL
|
|
40
|
+
* scheme, plain http being refused outside shadow mode, cloudflare's account id
|
|
41
|
+
* — is enforced server-side by the same `validateJevConfig` the loader runs.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
45
|
+
import {
|
|
46
|
+
getJevSettingsAction,
|
|
47
|
+
type JevModelView,
|
|
48
|
+
type JevSettingsView,
|
|
49
|
+
} from "@/app/actions/get-jev-config";
|
|
50
|
+
import {
|
|
51
|
+
removeJevConfigAction,
|
|
52
|
+
saveJevConfigAction,
|
|
53
|
+
} from "@/app/actions/update-jev-config";
|
|
54
|
+
import { toast } from "@/app/components/toast";
|
|
55
|
+
|
|
56
|
+
const PROVIDERS = [
|
|
57
|
+
{ value: "typesafe", label: "typesafe" },
|
|
58
|
+
{ value: "openrouter", label: "openrouter" },
|
|
59
|
+
{ value: "vercel", label: "vercel ai gateway" },
|
|
60
|
+
{ value: "cloudflare", label: "cloudflare workers ai" },
|
|
61
|
+
{ value: "custom", label: "custom endpoint" },
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
const MODES = [
|
|
65
|
+
{ value: "enforce", label: "enforce — jev's verdict counts" },
|
|
66
|
+
{ value: "shadow", label: "shadow — log only, regex decides" },
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
/** "24h" / "7d" / "30m", for the stats window. */
|
|
70
|
+
function fmtWindow(ms: number): string {
|
|
71
|
+
const h = ms / 3_600_000;
|
|
72
|
+
if (Number.isInteger(h) && h >= 1) return h % 24 === 0 ? `${h / 24}d` : `${h}h`;
|
|
73
|
+
return `${Math.max(1, Math.round(ms / 60_000))}m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A rate as a whole percent. 0.0417 → "4%". */
|
|
77
|
+
function fmtRate(rate: number): string {
|
|
78
|
+
return `${Math.round(rate * 100)}%`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The model row. `withheld` is the case worth having a row for at all: the file
|
|
83
|
+
* holds something in that slot that does not look like a model id, which is
|
|
84
|
+
* what a key pasted one field off looks like, so it is described and not
|
|
85
|
+
* printed — and the server never sent it here to print. The loader's own reason
|
|
86
|
+
* and its fix are already on the page above, in `view.problem`.
|
|
87
|
+
*/
|
|
88
|
+
function fmtModel(model: JevModelView): string {
|
|
89
|
+
switch (model.kind) {
|
|
90
|
+
case "id":
|
|
91
|
+
return model.id;
|
|
92
|
+
case "withheld":
|
|
93
|
+
return "set to something that is not a model id — not shown here, in case it is a key";
|
|
94
|
+
default:
|
|
95
|
+
return "the provider's default";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface FormState {
|
|
100
|
+
provider: string;
|
|
101
|
+
baseUrl: string;
|
|
102
|
+
accountId: string;
|
|
103
|
+
mode: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formFrom(view: JevSettingsView): FormState {
|
|
107
|
+
return {
|
|
108
|
+
provider: view.provider ?? "typesafe",
|
|
109
|
+
baseUrl: view.baseUrl,
|
|
110
|
+
accountId: view.accountId,
|
|
111
|
+
mode: view.mode,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** One labelled control, laid out like the interval row above it. */
|
|
116
|
+
function Field({
|
|
117
|
+
id,
|
|
118
|
+
label,
|
|
119
|
+
hint,
|
|
120
|
+
children,
|
|
121
|
+
}: {
|
|
122
|
+
id: string;
|
|
123
|
+
label: string;
|
|
124
|
+
hint?: string;
|
|
125
|
+
children: ReactNode;
|
|
126
|
+
}) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="set-field">
|
|
129
|
+
<label className="set-field-label" htmlFor={id}>
|
|
130
|
+
{label}
|
|
131
|
+
</label>
|
|
132
|
+
{children}
|
|
133
|
+
{hint ? <span className="set-hint">{hint}</span> : null}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default function JevPanel({ initial }: { initial: JevSettingsView | null }) {
|
|
139
|
+
const [view, setView] = useState<JevSettingsView | null>(initial);
|
|
140
|
+
const [form, setForm] = useState<FormState>(
|
|
141
|
+
initial ? formFrom(initial) : { provider: "typesafe", baseUrl: "", accountId: "", mode: "enforce" },
|
|
142
|
+
);
|
|
143
|
+
const [token, setToken] = useState("");
|
|
144
|
+
const [busy, setBusy] = useState(false);
|
|
145
|
+
const [problem, setProblem] = useState<string | null>(null);
|
|
146
|
+
/**
|
|
147
|
+
* The server's `needsToken`: the one refusal whose remedy is this field, so it
|
|
148
|
+
* is marked rather than leaving the person to match the message to a control.
|
|
149
|
+
* The refusals that no token can settle deliberately do not set it.
|
|
150
|
+
*/
|
|
151
|
+
const [needsToken, setNeedsToken] = useState(false);
|
|
152
|
+
const mounted = useRef(true);
|
|
153
|
+
/**
|
|
154
|
+
* Whether the user has touched the form since the last read. A focus refresh
|
|
155
|
+
* must not overwrite a half-typed endpoint with what is still on disk — the
|
|
156
|
+
* page re-reads on every `visibilitychange`, which includes the tab hide that
|
|
157
|
+
* happens when somebody alt-tabs to their password manager mid-edit.
|
|
158
|
+
*/
|
|
159
|
+
const dirty = useRef(false);
|
|
160
|
+
|
|
161
|
+
const reload = useCallback(async () => {
|
|
162
|
+
try {
|
|
163
|
+
const next = await getJevSettingsAction();
|
|
164
|
+
if (!mounted.current) return;
|
|
165
|
+
setView(next);
|
|
166
|
+
if (!dirty.current) setForm(formFrom(next));
|
|
167
|
+
} catch {
|
|
168
|
+
// Leave what is on screen: it describes real machine state, and blanking
|
|
169
|
+
// it would report something less true than what is already there.
|
|
170
|
+
}
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
mounted.current = true;
|
|
175
|
+
void reload();
|
|
176
|
+
return () => {
|
|
177
|
+
mounted.current = false;
|
|
178
|
+
};
|
|
179
|
+
}, [reload]);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
// Same reason as the scheduled-audit panel: `failproofai jev setup` in a
|
|
183
|
+
// terminal changes the same file, and a page left open would otherwise keep
|
|
184
|
+
// showing the old state until it was reloaded by hand.
|
|
185
|
+
const onFocus = () => void reload();
|
|
186
|
+
window.addEventListener("focus", onFocus);
|
|
187
|
+
document.addEventListener("visibilitychange", onFocus);
|
|
188
|
+
return () => {
|
|
189
|
+
window.removeEventListener("focus", onFocus);
|
|
190
|
+
document.removeEventListener("visibilitychange", onFocus);
|
|
191
|
+
};
|
|
192
|
+
}, [reload]);
|
|
193
|
+
|
|
194
|
+
const set = useCallback(<K extends keyof FormState>(key: K, value: FormState[K]) => {
|
|
195
|
+
dirty.current = true;
|
|
196
|
+
setForm((f) => ({ ...f, [key]: value }));
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const onSave = useCallback(async () => {
|
|
200
|
+
setBusy(true);
|
|
201
|
+
setProblem(null);
|
|
202
|
+
try {
|
|
203
|
+
// The form does not offer a model, so it says nothing about one: the
|
|
204
|
+
// server keeps a stored model when the provider is unchanged, and drops it
|
|
205
|
+
// for the new provider's default when it is not — the same rule `jev setup`
|
|
206
|
+
// applies. `JevConfigInput` has no model field for this to get wrong.
|
|
207
|
+
const res = await saveJevConfigAction({
|
|
208
|
+
provider: form.provider,
|
|
209
|
+
baseUrl: form.baseUrl,
|
|
210
|
+
accountId: form.accountId,
|
|
211
|
+
mode: form.mode,
|
|
212
|
+
token,
|
|
213
|
+
});
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
setProblem(res.problem);
|
|
216
|
+
setNeedsToken(res.needsToken === true);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
dirty.current = false;
|
|
220
|
+
setNeedsToken(false);
|
|
221
|
+
setToken("");
|
|
222
|
+
setView(res.view);
|
|
223
|
+
setForm(formFrom(res.view));
|
|
224
|
+
toast(res.view.on ? "jev is on." : "saved.");
|
|
225
|
+
} catch {
|
|
226
|
+
setProblem("could not save that.");
|
|
227
|
+
} finally {
|
|
228
|
+
setBusy(false);
|
|
229
|
+
}
|
|
230
|
+
}, [form, token]);
|
|
231
|
+
|
|
232
|
+
const onRemove = useCallback(async () => {
|
|
233
|
+
setBusy(true);
|
|
234
|
+
setProblem(null);
|
|
235
|
+
setNeedsToken(false);
|
|
236
|
+
try {
|
|
237
|
+
const res = await removeJevConfigAction();
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
setProblem(res.problem);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
dirty.current = false;
|
|
243
|
+
setToken("");
|
|
244
|
+
setView(res.view);
|
|
245
|
+
setForm(formFrom(res.view));
|
|
246
|
+
toast("jev is off. hooks run the regex policies.");
|
|
247
|
+
} catch {
|
|
248
|
+
setProblem("could not turn that off.");
|
|
249
|
+
} finally {
|
|
250
|
+
setBusy(false);
|
|
251
|
+
}
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
const configured = view !== null && view.status !== "absent";
|
|
255
|
+
const stored = view?.token;
|
|
256
|
+
const tokenHint = needsToken
|
|
257
|
+
? "needed for this save — type the token for this endpoint"
|
|
258
|
+
: stored
|
|
259
|
+
? stored.source === "env"
|
|
260
|
+
? "configured, read from the environment — leave blank to keep it"
|
|
261
|
+
: stored.hint
|
|
262
|
+
? `configured, ending ${stored.hint} — leave blank to keep it`
|
|
263
|
+
: "configured — leave blank to keep it"
|
|
264
|
+
: // The file names the environment as the key's source and the variable is
|
|
265
|
+
// unset in this process. Leaving the field blank keeps that arrangement,
|
|
266
|
+
// which is not the same thing as "there is no key" — the endpoint and the
|
|
267
|
+
// mode are editable from here without one.
|
|
268
|
+
view?.status === "key-missing"
|
|
269
|
+
? "read from the environment, which is not set in this shell — leave blank to keep it that way"
|
|
270
|
+
: "sent as a bearer token; stored owner-only at 0600";
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="set-cell set-jev">
|
|
274
|
+
<h2 className="set-cell-title">jev · semantic evaluation</h2>
|
|
275
|
+
|
|
276
|
+
<div className="set-row set-row-main">
|
|
277
|
+
<div className="set-row-copy">
|
|
278
|
+
<span className={view?.on ? "set-strong" : "set-dim"}>
|
|
279
|
+
{view === null
|
|
280
|
+
? "reading…"
|
|
281
|
+
: view.on
|
|
282
|
+
? `on. every judged command is also asked of ${view.provider}.`
|
|
283
|
+
: "off. hooks run the regex policies exactly as before."}
|
|
284
|
+
</span>
|
|
285
|
+
<span className="set-dim">
|
|
286
|
+
{view?.on
|
|
287
|
+
? view.mode === "shadow"
|
|
288
|
+
? "shadow: jev's answers are logged, the regex result is what gets enforced."
|
|
289
|
+
: "enforce: jev's answers can clear a reviewable deny."
|
|
290
|
+
: "a second opinion on the commands the regex policies flag. needs your own endpoint and token."}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{view && configured && (
|
|
296
|
+
<dl className="set-how-list">
|
|
297
|
+
{view.endpoint && (
|
|
298
|
+
<div className="set-how-row">
|
|
299
|
+
<dt className="set-how-label">endpoint</dt>
|
|
300
|
+
<dd className="set-how-body">{view.endpoint}</dd>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
<div className="set-how-row">
|
|
304
|
+
<dt className="set-how-label">model</dt>
|
|
305
|
+
<dd className="set-how-body">{fmtModel(view.model)}</dd>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="set-how-row">
|
|
308
|
+
<dt className="set-how-label">token</dt>
|
|
309
|
+
<dd className="set-how-body">
|
|
310
|
+
{stored
|
|
311
|
+
? stored.source === "env"
|
|
312
|
+
? "from FAILPROOFAI_JEV_API_KEY"
|
|
313
|
+
: stored.hint
|
|
314
|
+
? `configured, ending ${stored.hint}`
|
|
315
|
+
: "configured"
|
|
316
|
+
: "none stored"}
|
|
317
|
+
</dd>
|
|
318
|
+
</div>
|
|
319
|
+
{view.permissions && (
|
|
320
|
+
<div className="set-how-row">
|
|
321
|
+
<dt className="set-how-label">file</dt>
|
|
322
|
+
<dd className="set-how-body">
|
|
323
|
+
{`${view.path} · ${view.permissions}`}
|
|
324
|
+
</dd>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
{view.reviewable && (
|
|
328
|
+
<div className="set-how-row">
|
|
329
|
+
<dt className="set-how-label">reviewable</dt>
|
|
330
|
+
{/* The count, in the server's words — the same sentence
|
|
331
|
+
`failproofai jev status` prints, because the two surfaces must
|
|
332
|
+
not disagree about what Jev is allowed to clear. */}
|
|
333
|
+
<dd className="set-how-body">{view.reviewable.summary}</dd>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{view.stats && view.stats.total > 0 && (
|
|
337
|
+
<div className="set-how-row">
|
|
338
|
+
<dt className="set-how-label">fallbacks</dt>
|
|
339
|
+
{/* One string, not an interpolated fragment: a sentence split
|
|
340
|
+
across text nodes is a sentence a screen reader and a test both
|
|
341
|
+
have to reassemble. */}
|
|
342
|
+
<dd className="set-how-body">
|
|
343
|
+
{`${fmtRate(view.stats.fallbackRate)} of ${view.stats.total} calls in the last ${fmtWindow(view.stats.windowMs)} fell back to the regex engine.`}
|
|
344
|
+
</dd>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</dl>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{view?.problem && (
|
|
351
|
+
<p className="set-warn">
|
|
352
|
+
{view.problem}
|
|
353
|
+
{view.fix ? ` — ${view.fix}` : ""}
|
|
354
|
+
</p>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Jev is on, answering, and cannot clear a single verdict. Nothing else
|
|
358
|
+
on this page shows it: a policy set with no authority marks behaves
|
|
359
|
+
exactly like a healthy one until you notice that nothing is ever
|
|
360
|
+
cleared. The server sends the cause and the remedy together. */}
|
|
361
|
+
{view?.reviewable?.problem && <p className="set-warn">{view.reviewable.problem}</p>}
|
|
362
|
+
|
|
363
|
+
<div className="set-rule" />
|
|
364
|
+
|
|
365
|
+
<div className="set-jev-fields">
|
|
366
|
+
<Field id="jev-provider" label="provider">
|
|
367
|
+
<select
|
|
368
|
+
id="jev-provider"
|
|
369
|
+
className="set-select"
|
|
370
|
+
value={form.provider}
|
|
371
|
+
disabled={busy}
|
|
372
|
+
onChange={(e) => set("provider", e.target.value)}
|
|
373
|
+
>
|
|
374
|
+
{PROVIDERS.map((p) => (
|
|
375
|
+
<option key={p.value} value={p.value}>
|
|
376
|
+
{p.label}
|
|
377
|
+
</option>
|
|
378
|
+
))}
|
|
379
|
+
</select>
|
|
380
|
+
</Field>
|
|
381
|
+
|
|
382
|
+
<Field
|
|
383
|
+
id="jev-url"
|
|
384
|
+
label="endpoint url"
|
|
385
|
+
hint={
|
|
386
|
+
form.provider === "custom"
|
|
387
|
+
? "required — https, or http to localhost in shadow mode"
|
|
388
|
+
: "leave blank for the provider's own api"
|
|
389
|
+
}
|
|
390
|
+
>
|
|
391
|
+
<input
|
|
392
|
+
id="jev-url"
|
|
393
|
+
type="text"
|
|
394
|
+
className="set-text"
|
|
395
|
+
inputMode="url"
|
|
396
|
+
autoComplete="off"
|
|
397
|
+
spellCheck={false}
|
|
398
|
+
placeholder={form.provider === "custom" ? "https://…" : "the provider's own api"}
|
|
399
|
+
value={form.baseUrl}
|
|
400
|
+
disabled={busy}
|
|
401
|
+
onChange={(e) => set("baseUrl", e.target.value)}
|
|
402
|
+
/>
|
|
403
|
+
</Field>
|
|
404
|
+
|
|
405
|
+
{form.provider === "cloudflare" && (
|
|
406
|
+
<Field id="jev-account" label="account id" hint="32 lowercase hex characters">
|
|
407
|
+
<input
|
|
408
|
+
id="jev-account"
|
|
409
|
+
type="text"
|
|
410
|
+
className="set-text"
|
|
411
|
+
autoComplete="off"
|
|
412
|
+
spellCheck={false}
|
|
413
|
+
value={form.accountId}
|
|
414
|
+
disabled={busy}
|
|
415
|
+
onChange={(e) => set("accountId", e.target.value)}
|
|
416
|
+
/>
|
|
417
|
+
</Field>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
<Field id="jev-token" label="token" hint={tokenHint}>
|
|
421
|
+
<input
|
|
422
|
+
id="jev-token"
|
|
423
|
+
type="password"
|
|
424
|
+
className="set-text"
|
|
425
|
+
autoComplete="off"
|
|
426
|
+
spellCheck={false}
|
|
427
|
+
placeholder={stored ? "•••• kept" : ""}
|
|
428
|
+
value={token}
|
|
429
|
+
disabled={busy}
|
|
430
|
+
aria-invalid={needsToken || undefined}
|
|
431
|
+
onChange={(e) => {
|
|
432
|
+
dirty.current = true;
|
|
433
|
+
setToken(e.target.value);
|
|
434
|
+
}}
|
|
435
|
+
/>
|
|
436
|
+
</Field>
|
|
437
|
+
|
|
438
|
+
<Field id="jev-mode" label="mode">
|
|
439
|
+
<select
|
|
440
|
+
id="jev-mode"
|
|
441
|
+
className="set-select"
|
|
442
|
+
value={form.mode}
|
|
443
|
+
disabled={busy}
|
|
444
|
+
onChange={(e) => set("mode", e.target.value)}
|
|
445
|
+
>
|
|
446
|
+
{MODES.map((m) => (
|
|
447
|
+
<option key={m.value} value={m.value}>
|
|
448
|
+
{m.label}
|
|
449
|
+
</option>
|
|
450
|
+
))}
|
|
451
|
+
</select>
|
|
452
|
+
</Field>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{problem && <p className="set-warn">{problem}</p>}
|
|
456
|
+
|
|
457
|
+
<div className="set-actions set-jev-actions">
|
|
458
|
+
<button type="button" className="btn btn-press" disabled={busy} onClick={() => void onSave()}>
|
|
459
|
+
{busy ? "[ saving… ]" : configured ? "[ save changes ]" : "[ turn jev on ]"}
|
|
460
|
+
</button>
|
|
461
|
+
{configured && (
|
|
462
|
+
<button type="button" className="set-link" disabled={busy} onClick={() => void onRemove()}>
|
|
463
|
+
turn jev off
|
|
464
|
+
</button>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -206,7 +206,24 @@ const HOW_IT_WORKS: ReadonlyArray<{ label: string; body: string }> = [
|
|
|
206
206
|
},
|
|
207
207
|
];
|
|
208
208
|
|
|
209
|
-
export default function SettingsClient({
|
|
209
|
+
export default function SettingsClient({
|
|
210
|
+
initial,
|
|
211
|
+
jevPanel,
|
|
212
|
+
}: {
|
|
213
|
+
initial: ScheduledAuditView | null;
|
|
214
|
+
/**
|
|
215
|
+
* The Jev panel, rendered by the server page and handed in as an element
|
|
216
|
+
* rather than imported here.
|
|
217
|
+
*
|
|
218
|
+
* It is its own feature with its own file, its own actions and its own
|
|
219
|
+
* refresh — nothing on this page reads or writes what it does. Composing it
|
|
220
|
+
* in keeps that true in the code as well as in the design: this component
|
|
221
|
+
* does not import the Jev actions, so a test of the scheduled-audit panel
|
|
222
|
+
* neither renders nor touches Jev, and vice versa. Optional so the existing
|
|
223
|
+
* callers that only care about the audit panel keep working unchanged.
|
|
224
|
+
*/
|
|
225
|
+
jevPanel?: ReactNode;
|
|
226
|
+
}) {
|
|
210
227
|
// Seeded from the server render, so the first paint already tells the truth
|
|
211
228
|
// about whether scheduled audits are on. See the note in `page.tsx`.
|
|
212
229
|
const [view, setView] = useState<ScheduledAuditView | null>(initial);
|
|
@@ -507,6 +524,10 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
|
|
|
507
524
|
dashboard can reach <code>~/.failproofai/config.json</code>.
|
|
508
525
|
</p>
|
|
509
526
|
</div>
|
|
527
|
+
{/* A different file. An unreadable `config.json` says nothing about
|
|
528
|
+
`jev.json`, and hiding the panel that can turn a second
|
|
529
|
+
evaluator off would be the worst moment to hide it. */}
|
|
530
|
+
{jevPanel}
|
|
510
531
|
</div>
|
|
511
532
|
) : (
|
|
512
533
|
<div className="set-console">
|
|
@@ -653,6 +674,11 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
|
|
|
653
674
|
</dl>
|
|
654
675
|
</div>
|
|
655
676
|
</div>
|
|
677
|
+
|
|
678
|
+
{/* Full width, below the two panels: Jev is a second evaluator for
|
|
679
|
+
the whole machine, not a property of the scheduled scan, and its
|
|
680
|
+
endpoint would not fit a half-width column anyway. */}
|
|
681
|
+
{jevPanel}
|
|
656
682
|
</div>
|
|
657
683
|
)}
|
|
658
684
|
|
|
@@ -151,6 +151,51 @@
|
|
|
151
151
|
margin: 0;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/* ── jev ──────────────────────────────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
/* A full-width cell in the same console, below the two panels. The gap above it
|
|
157
|
+
* is the console's own 1px grid — this rule adds no border of its own. */
|
|
158
|
+
.set-jev {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Two columns at desktop: a provider or a mode needs about half the width an
|
|
165
|
+
* endpoint does, and stacking five controls in one column turns a short form
|
|
166
|
+
* into a scroll. */
|
|
167
|
+
.set-jev-fields {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
170
|
+
gap: 16px 20px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.set-field {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
}
|
|
179
|
+
.set-field-label {
|
|
180
|
+
font-family: var(--font-mono);
|
|
181
|
+
font-size: 9.5px;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
letter-spacing: 0.18em;
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
color: var(--dim);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.set-jev-actions {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 16px;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@media (max-width: 640px) {
|
|
196
|
+
.set-jev-fields { grid-template-columns: 1fr; }
|
|
197
|
+
}
|
|
198
|
+
|
|
154
199
|
/* ── masthead ─────────────────────────────────────────────────────────────── */
|
|
155
200
|
|
|
156
201
|
.set-mast {
|
|
@@ -228,6 +273,40 @@
|
|
|
228
273
|
outline-offset: 1px;
|
|
229
274
|
}
|
|
230
275
|
|
|
276
|
+
/* `.set-num`'s box, stretched. Same background, same hairline, same square
|
|
277
|
+
* corners and the same pink focus ring — a second field style would read as a
|
|
278
|
+
* second form on one page. */
|
|
279
|
+
.set-text,
|
|
280
|
+
.set-select {
|
|
281
|
+
font-family: var(--font-mono);
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
width: 100%;
|
|
284
|
+
padding: 6px 10px;
|
|
285
|
+
background: var(--bg);
|
|
286
|
+
border: 1px solid var(--line-2);
|
|
287
|
+
color: var(--ink);
|
|
288
|
+
border-radius: 0;
|
|
289
|
+
}
|
|
290
|
+
.set-text:focus-visible,
|
|
291
|
+
.set-select:focus-visible {
|
|
292
|
+
outline: 2px solid var(--accent-pink);
|
|
293
|
+
outline-offset: 1px;
|
|
294
|
+
}
|
|
295
|
+
.set-text:disabled,
|
|
296
|
+
.set-select:disabled {
|
|
297
|
+
opacity: 0.5;
|
|
298
|
+
cursor: not-allowed;
|
|
299
|
+
}
|
|
300
|
+
.set-text::placeholder {
|
|
301
|
+
color: var(--dim);
|
|
302
|
+
}
|
|
303
|
+
/* The native arrow is drawn by the platform and cannot be tokenised; leaving
|
|
304
|
+
* the control's own appearance in place is the one honest option, so the box
|
|
305
|
+
* around it is what carries the page's style. */
|
|
306
|
+
.set-select {
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
}
|
|
309
|
+
|
|
231
310
|
.set-link {
|
|
232
311
|
font-family: var(--font-mono);
|
|
233
312
|
font-size: 12px;
|