failproofai 1.0.0 → 1.0.1-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/app-path-routes-manifest.json +0 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +0 -6
- 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 -3
- 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 -3
- 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 +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 +3 -3
- 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 +2 -2
- 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 +4 -3
- 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 +2 -2
- 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 +3 -2
- 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 +4 -3
- 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 +5 -3
- 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 +4 -3
- 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 +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +6 -5
- 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 +5 -4
- 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 +6 -4
- 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 +5 -4
- 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 +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +6 -4
- 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/app-paths-manifest.json +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- 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_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
- 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-config_ts_04t589g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
- 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 +15 -15
- package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
- package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
- package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
- package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
- package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
- package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
- package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
- package/.next/standalone/app/api/auth/status/route.ts +7 -17
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
- package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/audit-styles.css +150 -26
- package/.next/standalone/app/globals.css +39 -1
- package/.next/standalone/app/settings/page.tsx +38 -21
- package/.next/standalone/app/settings/settings-client.tsx +583 -392
- package/.next/standalone/app/settings/settings.css +354 -0
- package/.next/standalone/components/navbar.tsx +33 -4
- package/.next/standalone/lib/auth/api-server-client.ts +51 -29
- package/.next/standalone/lib/auth/auth-store.ts +44 -59
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +6 -0
- package/bin/failproofaid-shim.mjs +0 -0
- package/dist/cli.mjs +1889 -426
- package/dist/worker.mjs +27 -5
- package/lib/auth/api-server-client.ts +51 -29
- package/lib/auth/auth-store.ts +44 -59
- package/package.json +12 -10
- package/scripts/docs-audit.ts +493 -0
- package/scripts/translate-docs/cli.ts +16 -2
- package/scripts/translate-docs/mdx-translator.ts +6 -1
- package/scripts/translate-docs/readme-translator.ts +4 -2
- package/src/audit/cli-login.ts +330 -0
- package/src/audit/cli.ts +124 -6
- package/src/audit/dashboard-cache.ts +44 -8
- package/src/audit/harm-report.ts +260 -0
- package/src/audit/machine-store.ts +120 -0
- package/src/audit/redact-example.ts +312 -0
- package/src/audit/report-harm.ts +167 -0
- package/src/audit/schedule-cli.ts +352 -0
- package/src/hooks/builtin-policies.ts +26 -0
- package/src/hooks/daemon-service.ts +76 -0
- package/src/hooks/fp-config.ts +96 -5
- package/src/hooks/fp-home.ts +118 -8
- package/src/hooks/fp-reset.ts +79 -9
- package/src/hooks/migrations.ts +241 -4
- package/src/hooks/tui.ts +61 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
- package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
- package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
- package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
- package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
- package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
- package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
- package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
- package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
|
@@ -1,31 +1,59 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* /settings
|
|
5
|
-
* degraded states that are most of the real screens: daemon not installed /
|
|
6
|
-
* stopped / unsupported, no scan ever run, a scan running now, a last run that
|
|
7
|
-
* failed, signed out, and not cloud-enrolled. Each is shown explicitly, because
|
|
8
|
-
* a missing control reads as a bug.
|
|
4
|
+
* /settings — what failproof does on its own, while you are not looking.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
* `.report`/`.section`/`.panel`/`.btn` classes from globals.css, the same
|
|
12
|
-
* emerald switch /policies uses (PolicyToggle), inline `var(--…)` colours, and
|
|
13
|
-
* the shared `toast()`. No new design language, colour, or component library.
|
|
6
|
+
* ## The design
|
|
14
7
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* The subject is not a preferences form, it is a **console for a service running
|
|
9
|
+
* on your box**, so the page is built from what that service actually has: a
|
|
10
|
+
* state, a timer, a last result, and an identity it reports under. Two rows,
|
|
11
|
+
* drawn as one instrument:
|
|
12
|
+
*
|
|
13
|
+
* - a **stat row** — four cells, each a single fact, no two sharing a unit.
|
|
14
|
+
* It answers "what is happening right now" with no history and no chart.
|
|
15
|
+
* - a **panel row** — the controls on the left, what the scan actually does on
|
|
16
|
+
* the right, as three labelled lines rather than the paragraph they used to
|
|
17
|
+
* be. Same words, skimmable.
|
|
18
|
+
*
|
|
19
|
+
* The hairlines between cells are one `gap: 1px` over a line-coloured
|
|
20
|
+
* background, not per-cell borders, so every rule is exactly one pixel and they
|
|
21
|
+
* cannot double up where cells meet.
|
|
22
|
+
*
|
|
23
|
+
* **The signature is still the schedule tape**, now under the panels. A scan on
|
|
24
|
+
* a timer has one fact no number can express — where you are between the last
|
|
25
|
+
* scan and the next — so it is drawn, and it draws nothing when there aren't two
|
|
26
|
+
* real ends to sit between.
|
|
27
|
+
*
|
|
28
|
+
* Everything is existing tokens: the charcoal stack, pink for the control that
|
|
29
|
+
* acts and for anything wrong, mint for the thing that is alive. No third hue,
|
|
30
|
+
* no new webfont, one hard pixel shadow on the console. That is the budget.
|
|
31
|
+
*
|
|
32
|
+
* ## One switch, not two
|
|
33
|
+
*
|
|
34
|
+
* Scheduling and mailing are the same decision — the reason to put a scan on a
|
|
35
|
+
* timer is to be told what it found. So there is one toggle, it requires a
|
|
36
|
+
* sign-in, and "signed out with the timer on" is a real state the page names
|
|
37
|
+
* rather than a contradiction it prevents.
|
|
18
38
|
*/
|
|
19
39
|
|
|
20
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
-
import {
|
|
22
|
-
|
|
40
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
41
|
+
import {
|
|
42
|
+
getScheduledAuditAction,
|
|
43
|
+
type ScheduledAuditView,
|
|
44
|
+
} from "@/app/actions/get-scheduled-audit";
|
|
45
|
+
import {
|
|
46
|
+
setAutoAuditAction,
|
|
47
|
+
setAuditIntervalAction,
|
|
48
|
+
} from "@/app/actions/update-scheduled-audit";
|
|
23
49
|
import { triggerRun, RerunError } from "@/app/audit/_components/rerun-button";
|
|
50
|
+
import { AuthDialog, type AuthedUser } from "@/app/audit/_components/auth-dialog";
|
|
24
51
|
import { toast } from "@/app/components/toast";
|
|
25
|
-
import { fetchWithTimeout } from "@/lib/fetch-with-timeout";
|
|
26
52
|
import { formatRelativeTime } from "@/lib/format-duration";
|
|
53
|
+
import "./settings.css";
|
|
27
54
|
|
|
28
|
-
|
|
55
|
+
const MIN_INTERVAL_DAYS = 1;
|
|
56
|
+
const MAX_INTERVAL_DAYS = 90;
|
|
29
57
|
|
|
30
58
|
function fmtAbsolute(ms: number): string {
|
|
31
59
|
return new Date(ms).toLocaleString(undefined, {
|
|
@@ -36,18 +64,98 @@ function fmtAbsolute(ms: number): string {
|
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
/** "
|
|
40
|
-
function
|
|
41
|
-
const diff = ms -
|
|
67
|
+
/** "6d 4h" / "3h" / "12m" / "now". `formatRelativeTime` only speaks past. */
|
|
68
|
+
function fmtUntil(ms: number, now: number): string {
|
|
69
|
+
const diff = ms - now;
|
|
42
70
|
if (diff <= 0) return "now";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
71
|
+
const d = Math.floor(diff / 86_400_000);
|
|
72
|
+
const h = Math.floor((diff % 86_400_000) / 3_600_000);
|
|
73
|
+
if (d > 0) return h > 0 ? `${d}d ${h}h` : `${d}d`;
|
|
74
|
+
if (h > 0) return `${h}h`;
|
|
75
|
+
return `${Math.max(1, Math.floor(diff / 60_000))}m`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** "11d" / "4h" / "9m" — how long the service has been up. */
|
|
79
|
+
function fmtSpan(ms: number): string {
|
|
80
|
+
const d = Math.floor(ms / 86_400_000);
|
|
81
|
+
if (d > 0) return `${d}d`;
|
|
82
|
+
const h = Math.floor(ms / 3_600_000);
|
|
83
|
+
if (h > 0) return `${h}h`;
|
|
84
|
+
return `${Math.max(1, Math.floor(ms / 60_000))}m`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function num(n: number): string {
|
|
88
|
+
return n.toLocaleString("en-US");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One cell of the stat row: a label, one value, one sub-line.
|
|
93
|
+
*
|
|
94
|
+
* `tone` colours the VALUE only — mint for alive, pink for anything that needs a
|
|
95
|
+
* person. The label and sub-line stay in the ink ramp, so a row of four cells
|
|
96
|
+
* reads as one instrument rather than four competing signals.
|
|
97
|
+
*/
|
|
98
|
+
function StatCell({
|
|
99
|
+
label,
|
|
100
|
+
value,
|
|
101
|
+
sub,
|
|
102
|
+
tone,
|
|
103
|
+
}: {
|
|
104
|
+
label: string;
|
|
105
|
+
value: ReactNode;
|
|
106
|
+
sub?: ReactNode;
|
|
107
|
+
tone?: "ok" | "warn";
|
|
108
|
+
}) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="set-stat">
|
|
111
|
+
<div className="set-stat-label">{label}</div>
|
|
112
|
+
<div className={`set-stat-value${tone ? ` ${tone}` : ""}`}>{value}</div>
|
|
113
|
+
<div className="set-stat-sub">{sub ?? " "}</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
46
116
|
}
|
|
47
117
|
|
|
48
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The schedule tape — where this machine is between two scans.
|
|
120
|
+
*
|
|
121
|
+
* Drawn rather than stated because the fact is a POSITION, and a position is
|
|
122
|
+
* the one thing a number cannot show at a glance. The filled span is elapsed,
|
|
123
|
+
* the marker is now, the ends are the two scans.
|
|
124
|
+
*
|
|
125
|
+
* Renders nothing without both ends: a machine that has never run a scheduled
|
|
126
|
+
* scan has no interval to be inside, and an empty rail claiming otherwise would
|
|
127
|
+
* be decoration.
|
|
128
|
+
*/
|
|
129
|
+
function ScheduleTape({
|
|
130
|
+
lastRunAtMs,
|
|
131
|
+
nextDueAtMs,
|
|
132
|
+
now,
|
|
133
|
+
}: {
|
|
134
|
+
lastRunAtMs: number | null;
|
|
135
|
+
nextDueAtMs: number | null;
|
|
136
|
+
/** Stamped by the parent on load and on every focus refresh. Passed in
|
|
137
|
+
* rather than read here so this component stays pure during render — and so
|
|
138
|
+
* the marker moves when the page is refocused, which is the only moment
|
|
139
|
+
* anyone is looking at it. */
|
|
140
|
+
now: number;
|
|
141
|
+
}) {
|
|
142
|
+
if (lastRunAtMs == null || nextDueAtMs == null || nextDueAtMs <= lastRunAtMs) return null;
|
|
143
|
+
const pct = Math.min(100, Math.max(0, ((now - lastRunAtMs) / (nextDueAtMs - lastRunAtMs)) * 100));
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="tape" aria-hidden="true">
|
|
147
|
+
<div className="tape-rail">
|
|
148
|
+
<div className="tape-fill" style={{ width: `${pct}%` }} />
|
|
149
|
+
<div className="tape-now" style={{ left: `${pct}%` }} />
|
|
150
|
+
</div>
|
|
151
|
+
<div className="tape-ends">
|
|
152
|
+
<span>last scan · {fmtAbsolute(lastRunAtMs)}</span>
|
|
153
|
+
<span className="tape-next">next · {fmtUntil(nextDueAtMs, now)}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
49
158
|
|
|
50
|
-
/** The exact switch /policies uses — copied shape, not a new control. */
|
|
51
159
|
function Toggle({
|
|
52
160
|
enabled,
|
|
53
161
|
onChange,
|
|
@@ -62,426 +170,509 @@ function Toggle({
|
|
|
62
170
|
return (
|
|
63
171
|
<button
|
|
64
172
|
type="button"
|
|
65
|
-
onClick={onChange}
|
|
66
|
-
disabled={disabled}
|
|
67
173
|
role="switch"
|
|
68
174
|
aria-checked={enabled}
|
|
69
175
|
aria-label={label}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
onClick={onChange}
|
|
178
|
+
className="sw"
|
|
179
|
+
data-on={enabled ? "true" : "false"}
|
|
73
180
|
>
|
|
74
|
-
<span
|
|
75
|
-
className={`inline-block h-3 w-3 rounded-full bg-white shadow transition-transform ${
|
|
76
|
-
enabled ? "translate-x-3.5" : "translate-x-0.5"
|
|
77
|
-
}`}
|
|
78
|
-
/>
|
|
181
|
+
<span className="sw-knob" />
|
|
79
182
|
</button>
|
|
80
183
|
);
|
|
81
184
|
}
|
|
82
185
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const SECTION_TITLE: React.CSSProperties = {
|
|
115
|
-
fontFamily: "var(--font-mono)",
|
|
116
|
-
fontSize: 16,
|
|
117
|
-
fontWeight: 600,
|
|
118
|
-
letterSpacing: "-0.01em",
|
|
119
|
-
color: "var(--ink)",
|
|
120
|
-
margin: "0 0 4px",
|
|
121
|
-
};
|
|
122
|
-
const BODY: React.CSSProperties = {
|
|
123
|
-
fontFamily: "var(--font-mono)",
|
|
124
|
-
fontSize: 13,
|
|
125
|
-
color: "var(--ink-2)",
|
|
126
|
-
lineHeight: 1.65,
|
|
127
|
-
margin: 0,
|
|
128
|
-
};
|
|
129
|
-
const MUTED: React.CSSProperties = { ...BODY, color: "var(--dim)", fontSize: 12 };
|
|
130
|
-
const CODE: React.CSSProperties = { color: "var(--ink)", fontVariantLigatures: "none" };
|
|
131
|
-
|
|
132
|
-
/** A monospace inline command the user can copy by eye. */
|
|
133
|
-
function Cmd({ children }: { children: React.ReactNode }) {
|
|
134
|
-
return (
|
|
135
|
-
<code
|
|
136
|
-
style={{
|
|
137
|
-
fontFamily: "var(--font-mono)",
|
|
138
|
-
fontSize: 12,
|
|
139
|
-
color: "var(--accent-green)",
|
|
140
|
-
background: "var(--bg-2)",
|
|
141
|
-
border: "1px solid var(--line-2)",
|
|
142
|
-
padding: "1px 6px",
|
|
143
|
-
whiteSpace: "nowrap",
|
|
144
|
-
}}
|
|
145
|
-
>
|
|
146
|
-
{children}
|
|
147
|
-
</code>
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── scheduled audit section ──────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
function ScheduledAuditSection({
|
|
154
|
-
view,
|
|
155
|
-
onReload,
|
|
156
|
-
}: {
|
|
157
|
-
view: ScheduledAuditView;
|
|
158
|
-
onReload: () => Promise<void>;
|
|
159
|
-
}) {
|
|
160
|
-
const [auto, setAuto] = useState(view.auto);
|
|
161
|
-
const [interval, setIntervalDays] = useState(view.intervalDays);
|
|
162
|
-
const [savingAuto, setSavingAuto] = useState(false);
|
|
163
|
-
const [savingInterval, setSavingInterval] = useState(false);
|
|
186
|
+
/**
|
|
187
|
+
* What the scan does, as three labelled lines. This is the old footer paragraph
|
|
188
|
+
* restructured — same claims, scannable instead of a wall.
|
|
189
|
+
*
|
|
190
|
+
* "sends" ENUMERATES rather than saying "only counts and redacted examples".
|
|
191
|
+
* That was very nearly true, and very nearly true is the worse kind: the report
|
|
192
|
+
* carries the machine's name too — its hostname, which routinely carries its
|
|
193
|
+
* owner's. A list a person can check beats a stronger claim they cannot, and
|
|
194
|
+
* this panel is the one place they would come to check. The digest email states
|
|
195
|
+
* the same three, in the same order.
|
|
196
|
+
*/
|
|
197
|
+
const HOW_IT_WORKS: ReadonlyArray<{ label: string; body: string }> = [
|
|
198
|
+
{
|
|
199
|
+
label: "reads",
|
|
200
|
+
body: "every session transcript on disk — your prompts, the files your agents read and wrote, and command output.",
|
|
201
|
+
},
|
|
202
|
+
{ label: "runs", body: "entirely on this machine. the transcripts never leave it." },
|
|
203
|
+
{
|
|
204
|
+
label: "sends",
|
|
205
|
+
body: "counts, redacted examples, and this machine's name — and only when a scan finds something harmful.",
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
export default function SettingsClient({ initial }: { initial: ScheduledAuditView | null }) {
|
|
210
|
+
// Seeded from the server render, so the first paint already tells the truth
|
|
211
|
+
// about whether scheduled audits are on. See the note in `page.tsx`.
|
|
212
|
+
const [view, setView] = useState<ScheduledAuditView | null>(initial);
|
|
213
|
+
const [auto, setAuto] = useState(initial?.auto ?? false);
|
|
214
|
+
const [intervalDays, setIntervalDays] = useState(initial?.intervalDays ?? 7);
|
|
215
|
+
const [busy, setBusy] = useState(false);
|
|
164
216
|
const [running, setRunning] = useState(false);
|
|
165
|
-
const [
|
|
217
|
+
const [authOpen, setAuthOpen] = useState(false);
|
|
218
|
+
const [loadError, setLoadError] = useState(initial === null);
|
|
219
|
+
/**
|
|
220
|
+
* When the view was last read. Drives the tape's marker and the uptime and
|
|
221
|
+
* countdown readouts; see ScheduleTape.
|
|
222
|
+
*
|
|
223
|
+
* Zero until the client has run, deliberately: `Date.now()` on the server and
|
|
224
|
+
* `Date.now()` in the browser are different clocks, and seeding this from the
|
|
225
|
+
* server render would put the marker at a position the client then corrects —
|
|
226
|
+
* a hydration mismatch on the one element whose whole point is a position.
|
|
227
|
+
* The tape appears on the first client pass instead.
|
|
228
|
+
*/
|
|
229
|
+
const [nowMs, setNowMs] = useState(0);
|
|
230
|
+
const mounted = useRef(true);
|
|
231
|
+
/**
|
|
232
|
+
* Whether anything is on screen to protect, readable from a stable callback.
|
|
233
|
+
*
|
|
234
|
+
* `reload` has an empty dep list on purpose (see below), so the `view` it
|
|
235
|
+
* closes over is frozen at the FIRST render forever. Testing that state
|
|
236
|
+
* directly therefore answered a question about page load, not about now: a
|
|
237
|
+
* page seeded with `initial === null` — the case `page.tsx` builds for when
|
|
238
|
+
* the server read fails — kept reading `!view` as true even after the client
|
|
239
|
+
* had successfully loaded, so the next transient failure (the focus listener
|
|
240
|
+
* below fires on every `visibilitychange`, including a tab hide) replaced a
|
|
241
|
+
* working console with "could not read this machine's settings". A ref is
|
|
242
|
+
* read at call time, which is when the question is being asked.
|
|
243
|
+
*/
|
|
244
|
+
const hasView = useRef(initial !== null);
|
|
166
245
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
246
|
+
const reload = useCallback(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const next = await getScheduledAuditAction();
|
|
249
|
+
if (!mounted.current) return;
|
|
250
|
+
// Every stat comes from this ONE call, so a finished scan updates the
|
|
251
|
+
// last-scan time, the finding count and the countdown in a single paint.
|
|
252
|
+
// Fetching them separately is how a page ends up showing a fresh
|
|
253
|
+
// timestamp beside a stale count.
|
|
254
|
+
setView(next);
|
|
255
|
+
hasView.current = true;
|
|
256
|
+
setAuto(next.auto);
|
|
257
|
+
setIntervalDays(next.intervalDays);
|
|
258
|
+
setNowMs(Date.now());
|
|
259
|
+
setLoadError(false);
|
|
260
|
+
} catch {
|
|
261
|
+
if (mounted.current && !hasView.current) setLoadError(true);
|
|
262
|
+
// An existing view is LEFT ALONE on a failed refresh: it describes real
|
|
263
|
+
// machine state, and blanking it would report something less true than
|
|
264
|
+
// what is already on screen.
|
|
265
|
+
}
|
|
266
|
+
// Empty on purpose, and now honestly so: reading `view` here would rebuild
|
|
267
|
+
// this callback on every load and re-fire the focus listener below, which is
|
|
268
|
+
// why the "is anything on screen" question goes through the ref above
|
|
269
|
+
// instead. Nothing reactive is left to declare, so the rule no longer needs
|
|
270
|
+
// suppressing — and the suppression had been hiding the stale read.
|
|
271
|
+
}, []);
|
|
171
272
|
|
|
172
|
-
// Reflect a scan already in flight (started here or from /audit) so the button
|
|
173
|
-
// and status line don't claim the machine is idle when it isn't.
|
|
174
273
|
useEffect(() => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const s = (await res.json()) as { running?: boolean };
|
|
181
|
-
setRunning(Boolean(s.running));
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
/* status is best-effort; a missing poll just means we assume idle */
|
|
185
|
-
}
|
|
186
|
-
})();
|
|
274
|
+
mounted.current = true;
|
|
275
|
+
// Still refreshes on mount even though the server seeded us: it stamps
|
|
276
|
+
// `nowMs` for the tape, and it picks up anything that changed between the
|
|
277
|
+
// server render and the browser getting here.
|
|
278
|
+
void reload();
|
|
187
279
|
return () => {
|
|
188
|
-
|
|
280
|
+
mounted.current = false;
|
|
189
281
|
};
|
|
190
|
-
}, []);
|
|
282
|
+
}, [reload]);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Re-read when the tab regains focus.
|
|
286
|
+
*
|
|
287
|
+
* The CLI writes the same `config.json` through the same `updateConfig`, so
|
|
288
|
+
* the two can never disagree on disk — but a page left open while somebody
|
|
289
|
+
* ran `failproofai audit --schedule 7` in a terminal would keep showing the
|
|
290
|
+
* old toggle. Refreshing on focus picks that up the moment you look at it,
|
|
291
|
+
* without a timer firing on a page that is usually idle.
|
|
292
|
+
*/
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const onFocus = () => void reload();
|
|
295
|
+
window.addEventListener("focus", onFocus);
|
|
296
|
+
document.addEventListener("visibilitychange", onFocus);
|
|
297
|
+
return () => {
|
|
298
|
+
window.removeEventListener("focus", onFocus);
|
|
299
|
+
document.removeEventListener("visibilitychange", onFocus);
|
|
300
|
+
};
|
|
301
|
+
}, [reload]);
|
|
191
302
|
|
|
192
|
-
const
|
|
193
|
-
const
|
|
303
|
+
const signedIn = view?.signedInAs ?? null;
|
|
304
|
+
const daemonRunning = view?.daemon === "running";
|
|
305
|
+
const daemonUnsupported = view?.daemon === "unsupported-platform";
|
|
306
|
+
const sched = view?.schedule ?? null;
|
|
307
|
+
const lastScan = view?.lastScan ?? null;
|
|
308
|
+
const lastExitBad =
|
|
309
|
+
sched?.lastExitCode != null && sched.lastExitCode !== 0 && sched.lastExitCode !== 75;
|
|
194
310
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
setAuto(next); // optimistic
|
|
198
|
-
setSavingAuto(true);
|
|
311
|
+
const enable = useCallback(async () => {
|
|
312
|
+
setBusy(true);
|
|
199
313
|
try {
|
|
200
|
-
const res = await setAutoAuditAction(
|
|
314
|
+
const res = await setAutoAuditAction(true);
|
|
315
|
+
if (!res.ok) {
|
|
316
|
+
setAuto(false);
|
|
317
|
+
if (res.reason === "unreachable") {
|
|
318
|
+
// The session is fine and the network is not. Offering a code prompt
|
|
319
|
+
// here would name a failure the user does not have and hand them a
|
|
320
|
+
// flow that cannot succeed either — and abandoning it mid-way is how
|
|
321
|
+
// a working session gets replaced with none at all.
|
|
322
|
+
toast("could not reach the server. check your connection and try again.");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// The server rejected the session this page had been showing an address
|
|
326
|
+
// for — expired, or minted against a different api-server. The local
|
|
327
|
+
// file is the only thing that said "signed in", and `whoAmI` has since
|
|
328
|
+
// cleared it, so re-read before opening the dialog: otherwise the page
|
|
329
|
+
// asks for an email while still displaying one.
|
|
330
|
+
await reload();
|
|
331
|
+
setAuthOpen(true);
|
|
332
|
+
toast("that sign-in expired. one more code and it's on.");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
201
335
|
setAuto(res.auto);
|
|
202
|
-
toast(
|
|
203
|
-
await
|
|
336
|
+
toast("scheduled audits on.");
|
|
337
|
+
await reload();
|
|
204
338
|
} catch {
|
|
205
|
-
setAuto(
|
|
206
|
-
toast("
|
|
339
|
+
setAuto(false);
|
|
340
|
+
toast("could not turn that on.");
|
|
207
341
|
} finally {
|
|
208
|
-
|
|
342
|
+
setBusy(false);
|
|
209
343
|
}
|
|
210
|
-
}, [
|
|
344
|
+
}, [reload]);
|
|
345
|
+
|
|
346
|
+
const onToggle = useCallback(async () => {
|
|
347
|
+
if (auto) {
|
|
348
|
+
setBusy(true);
|
|
349
|
+
try {
|
|
350
|
+
const res = await setAutoAuditAction(false);
|
|
351
|
+
// Turning it OFF is never refused, so `ok` is always true here — the
|
|
352
|
+
// narrowing is the type system's, not a case that can happen.
|
|
353
|
+
if (res.ok) setAuto(res.auto);
|
|
354
|
+
toast("scheduled audits off.");
|
|
355
|
+
await reload();
|
|
356
|
+
} catch {
|
|
357
|
+
toast("could not turn that off.");
|
|
358
|
+
} finally {
|
|
359
|
+
setBusy(false);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Turning it on needs somewhere to send the digest.
|
|
364
|
+
if (!signedIn) {
|
|
365
|
+
setAuthOpen(true);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await enable();
|
|
369
|
+
}, [auto, enable, reload, signedIn]);
|
|
211
370
|
|
|
212
371
|
const commitInterval = useCallback(
|
|
213
372
|
async (raw: number) => {
|
|
214
|
-
|
|
373
|
+
setBusy(true);
|
|
215
374
|
try {
|
|
216
|
-
// The config owns the 1..90 clamp; we reflect whatever it stored.
|
|
217
375
|
const res = await setAuditIntervalAction(raw);
|
|
376
|
+
// The action's return IS the authoritative value — it re-reads through
|
|
377
|
+
// `readConfig`, so it already carries the 1..90 clamp. No reload after:
|
|
378
|
+
// it would re-fetch the same number, and the schedule it would also
|
|
379
|
+
// re-read has not changed yet either, since the daemon recomputes the
|
|
380
|
+
// next due time on its own tick rather than when the interval is saved.
|
|
218
381
|
setIntervalDays(res.intervalDays);
|
|
219
|
-
|
|
382
|
+
// `view` is this page's mirror of what is ON DISK, and the write just
|
|
383
|
+
// changed disk — so it has to be told, even though nothing is re-read.
|
|
384
|
+
// The blur handler below skips the write when the typed value already
|
|
385
|
+
// equals `view.intervalDays`, and with the mirror left stale that guard
|
|
386
|
+
// compared against a number two edits old: type 14, blur, then type the
|
|
387
|
+
// original 7 back and the second blur was silently dropped. The input
|
|
388
|
+
// read 7, the config still said 14, and nothing said so until the next
|
|
389
|
+
// focus refresh flipped the field back.
|
|
390
|
+
setView((v) => (v ? { ...v, intervalDays: res.intervalDays } : v));
|
|
391
|
+
toast(`scanning every ${res.intervalDays} day${res.intervalDays === 1 ? "" : "s"}.`);
|
|
220
392
|
} catch {
|
|
221
|
-
setIntervalDays(view
|
|
222
|
-
toast("
|
|
393
|
+
setIntervalDays(view?.intervalDays ?? 7);
|
|
394
|
+
toast("could not save that.");
|
|
223
395
|
} finally {
|
|
224
|
-
|
|
396
|
+
setBusy(false);
|
|
225
397
|
}
|
|
226
398
|
},
|
|
227
|
-
[view
|
|
399
|
+
[view?.intervalDays],
|
|
228
400
|
);
|
|
229
401
|
|
|
402
|
+
const onSignOut = useCallback(async () => {
|
|
403
|
+
setBusy(true);
|
|
404
|
+
try {
|
|
405
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
406
|
+
toast("signed out. scans continue; digests pause.");
|
|
407
|
+
await reload();
|
|
408
|
+
} catch {
|
|
409
|
+
toast("could not sign out.");
|
|
410
|
+
} finally {
|
|
411
|
+
setBusy(false);
|
|
412
|
+
}
|
|
413
|
+
}, [reload]);
|
|
414
|
+
|
|
230
415
|
const onRunNow = useCallback(async () => {
|
|
231
|
-
if (
|
|
232
|
-
setRunningNow(true);
|
|
416
|
+
if (running) return;
|
|
233
417
|
setRunning(true);
|
|
234
418
|
try {
|
|
235
419
|
await triggerRun({ cli: [], since: "all", noCache: false });
|
|
236
|
-
toast("
|
|
237
|
-
await
|
|
420
|
+
toast("scan complete.");
|
|
421
|
+
await reload();
|
|
238
422
|
} catch (err) {
|
|
239
|
-
|
|
423
|
+
toast(
|
|
240
424
|
err instanceof RerunError && err.kind === "timeout"
|
|
241
|
-
? "
|
|
242
|
-
: "
|
|
243
|
-
|
|
425
|
+
? "the scan is taking a while — it will finish in the background."
|
|
426
|
+
: "the scan could not be completed.",
|
|
427
|
+
);
|
|
244
428
|
} finally {
|
|
245
|
-
setRunningNow(false);
|
|
246
429
|
setRunning(false);
|
|
247
430
|
}
|
|
248
|
-
}, [
|
|
249
|
-
|
|
250
|
-
const sched = view.schedule;
|
|
251
|
-
const lastExitBad =
|
|
252
|
-
sched?.lastExitCode != null && sched.lastExitCode !== 0 && sched.lastExitCode !== 75;
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<div className="panel" style={{ padding: 20, marginBottom: 20 }}>
|
|
256
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
|
257
|
-
<div>
|
|
258
|
-
<h3 style={SECTION_TITLE}>Scheduled audit</h3>
|
|
259
|
-
<p style={MUTED}>Scan this machine on a timer, in the background.</p>
|
|
260
|
-
</div>
|
|
261
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
262
|
-
{view.daemon === "running" && <Pill tone="ok">daemon running</Pill>}
|
|
263
|
-
{view.daemon === "stopped" && <Pill tone="warn">daemon stopped</Pill>}
|
|
264
|
-
{view.daemon === "not-installed" && <Pill tone="warn">daemon not installed</Pill>}
|
|
265
|
-
{view.daemon === "unsupported-platform" && <Pill tone="muted">daemon unavailable</Pill>}
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
{/* Enable toggle + the plain statement about what the scan reads. */}
|
|
270
|
-
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginTop: 18 }}>
|
|
271
|
-
<div style={{ marginTop: 2 }}>
|
|
272
|
-
<Toggle
|
|
273
|
-
enabled={auto}
|
|
274
|
-
onChange={onToggleAuto}
|
|
275
|
-
disabled={savingAuto || daemonUnsupported}
|
|
276
|
-
label={auto ? "Turn off scheduled scanning" : "Turn on scheduled scanning"}
|
|
277
|
-
/>
|
|
278
|
-
</div>
|
|
279
|
-
<div style={{ minWidth: 0 }}>
|
|
280
|
-
<p style={{ ...BODY, color: "var(--ink)" }}>
|
|
281
|
-
{auto ? "Scanning this machine on a schedule." : "Scan this machine on a schedule."}
|
|
282
|
-
</p>
|
|
283
|
-
<p style={{ ...BODY, marginTop: 6 }}>
|
|
284
|
-
The scan reads the <span style={CODE}>contents</span> of every session transcript on
|
|
285
|
-
disk across all installed agent CLIs — your prompts, the files they read and wrote,
|
|
286
|
-
and command output. It runs entirely on this machine. Nothing is sent anywhere unless
|
|
287
|
-
you also turn on emailed reports below.
|
|
288
|
-
</p>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
|
-
{/* Interval. The number bounds mirror the config's own 1..90 clamp as a UX
|
|
293
|
-
hint; the config remains the authority and we reflect what it stored. */}
|
|
294
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 16, flexWrap: "wrap" }}>
|
|
295
|
-
<label htmlFor="scan-interval" style={{ ...BODY }}>
|
|
296
|
-
Scan every
|
|
297
|
-
</label>
|
|
298
|
-
<input
|
|
299
|
-
id="scan-interval"
|
|
300
|
-
type="number"
|
|
301
|
-
min={1}
|
|
302
|
-
max={90}
|
|
303
|
-
step={1}
|
|
304
|
-
value={interval}
|
|
305
|
-
disabled={savingInterval || daemonUnsupported}
|
|
306
|
-
onChange={(e) => setIntervalDays(Number(e.target.value))}
|
|
307
|
-
onBlur={(e) => {
|
|
308
|
-
const v = Number(e.target.value);
|
|
309
|
-
// A cleared/garbage field must not persist NaN — snap back to the
|
|
310
|
-
// stored value and let the config keep owning the real bounds.
|
|
311
|
-
if (!Number.isFinite(v)) {
|
|
312
|
-
setIntervalDays(view.intervalDays);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (v !== view.intervalDays) void commitInterval(v);
|
|
316
|
-
}}
|
|
317
|
-
style={{
|
|
318
|
-
width: 64,
|
|
319
|
-
padding: "6px 8px",
|
|
320
|
-
background: "var(--bg)",
|
|
321
|
-
border: "1px solid var(--line-2)",
|
|
322
|
-
color: "var(--ink)",
|
|
323
|
-
fontFamily: "var(--font-mono)",
|
|
324
|
-
fontSize: 13,
|
|
325
|
-
textAlign: "center",
|
|
326
|
-
}}
|
|
327
|
-
/>
|
|
328
|
-
<span style={BODY}>day{interval === 1 ? "" : "s"}.</span>
|
|
329
|
-
<span style={MUTED}>1–90; the config keeps it in range.</span>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
{/* Last run / next due — read from the daemon-written schedule file. */}
|
|
333
|
-
<div
|
|
334
|
-
style={{
|
|
335
|
-
marginTop: 18,
|
|
336
|
-
paddingTop: 16,
|
|
337
|
-
borderTop: "1px dashed var(--line)",
|
|
338
|
-
display: "flex",
|
|
339
|
-
flexDirection: "column",
|
|
340
|
-
gap: 6,
|
|
341
|
-
}}
|
|
342
|
-
>
|
|
343
|
-
{running && (
|
|
344
|
-
<p style={{ ...BODY, color: "var(--accent-green)" }}>A scan is running now…</p>
|
|
345
|
-
)}
|
|
431
|
+
}, [reload, running]);
|
|
346
432
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
) : view.lastResultAt ? (
|
|
355
|
-
<p style={BODY}>
|
|
356
|
-
Last audit result:{" "}
|
|
357
|
-
<span style={CODE}>{fmtAbsolute(new Date(view.lastResultAt).getTime())}</span>{" "}
|
|
358
|
-
<span style={MUTED}>(no scheduled scan has run yet)</span>
|
|
359
|
-
</p>
|
|
360
|
-
) : (
|
|
361
|
-
<p style={BODY}>No scan has run yet.</p>
|
|
362
|
-
)}
|
|
363
|
-
|
|
364
|
-
{/* Next due */}
|
|
365
|
-
{auto ? (
|
|
366
|
-
sched?.nextDueAtMs != null ? (
|
|
367
|
-
<p style={BODY}>
|
|
368
|
-
Next scan due:{" "}
|
|
369
|
-
<span style={CODE}>{fmtAbsolute(sched.nextDueAtMs)}</span>{" "}
|
|
370
|
-
<span style={MUTED}>({fmtFuture(sched.nextDueAtMs)})</span>
|
|
371
|
-
</p>
|
|
372
|
-
) : (
|
|
373
|
-
<p style={BODY}>
|
|
374
|
-
Next scan:{" "}
|
|
375
|
-
<span style={MUTED}>the daemon will schedule it shortly.</span>
|
|
376
|
-
</p>
|
|
377
|
-
)
|
|
378
|
-
) : (
|
|
379
|
-
<p style={MUTED}>Scheduled scanning is off — no scan is scheduled.</p>
|
|
380
|
-
)}
|
|
381
|
-
|
|
382
|
-
{lastExitBad && (
|
|
383
|
-
<p style={{ ...BODY, color: "var(--amber)" }}>
|
|
384
|
-
The last scheduled scan exited with code {sched?.lastExitCode}. It will retry on the
|
|
385
|
-
next tick.
|
|
386
|
-
</p>
|
|
387
|
-
)}
|
|
388
|
-
{sched?.schemaAhead && (
|
|
389
|
-
<p style={MUTED}>
|
|
390
|
-
A newer daemon wrote this schedule; some fields may not be shown.
|
|
391
|
-
</p>
|
|
392
|
-
)}
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
{/* Degraded daemon guidance — say plainly why "on" may still not run. */}
|
|
396
|
-
{auto && daemonInactive && (
|
|
397
|
-
<p style={{ ...BODY, color: "var(--amber)", marginTop: 14 }}>
|
|
398
|
-
{daemonUnsupported ? (
|
|
399
|
-
<>The background daemon isn't available on this platform, so scheduled scans
|
|
400
|
-
can't run here. You can still run one now, and use the audit page.</>
|
|
401
|
-
) : view.daemon === "not-installed" ? (
|
|
402
|
-
<>Scheduled scanning is on, but the background service isn't installed, so nothing
|
|
403
|
-
will run on the timer yet. Install it with <Cmd>failproofai config</Cmd>.</>
|
|
404
|
-
) : (
|
|
405
|
-
<>Scheduled scanning is on, but the background service is stopped, so nothing will run
|
|
406
|
-
until it starts. Reinstall or repair it with <Cmd>failproofai config</Cmd>.</>
|
|
407
|
-
)}
|
|
408
|
-
</p>
|
|
409
|
-
)}
|
|
410
|
-
|
|
411
|
-
{/* Run now — reuses the existing /api/audit/run route via triggerRun. */}
|
|
412
|
-
<div style={{ marginTop: 18 }}>
|
|
413
|
-
<button
|
|
414
|
-
type="button"
|
|
415
|
-
className="btn"
|
|
416
|
-
onClick={onRunNow}
|
|
417
|
-
disabled={runningNow || running}
|
|
418
|
-
>
|
|
419
|
-
{runningNow || running ? "[ scanning… ]" : "[ run a scan now ]"}
|
|
420
|
-
</button>
|
|
421
|
-
</div>
|
|
422
|
-
</div>
|
|
433
|
+
const onAuthed = useCallback(
|
|
434
|
+
async (_user: AuthedUser) => {
|
|
435
|
+
setAuthOpen(false);
|
|
436
|
+
await reload();
|
|
437
|
+
await enable();
|
|
438
|
+
},
|
|
439
|
+
[enable, reload],
|
|
423
440
|
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ── page ─────────────────────────────────────────────────────────────────────
|
|
427
441
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
// ── the four stats ────────────────────────────────────────────────────────
|
|
443
|
+
// Each is one fact from one source. Where a value is unknown it says so; "—"
|
|
444
|
+
// and 0 are deliberately different claims, because a machine that scanned and
|
|
445
|
+
// found nothing is not the same as a file we could not read.
|
|
446
|
+
|
|
447
|
+
const daemonCell = (() => {
|
|
448
|
+
if (!view) return { value: "…", sub: "reading" } as const;
|
|
449
|
+
if (daemonRunning) {
|
|
450
|
+
const up =
|
|
451
|
+
view.daemonStartedAtMs != null && nowMs > 0
|
|
452
|
+
? `up ${fmtSpan(Math.max(0, nowMs - view.daemonStartedAtMs))}`
|
|
453
|
+
: "";
|
|
454
|
+
return { value: "running", sub: up, tone: "ok" as const };
|
|
455
|
+
}
|
|
456
|
+
if (daemonUnsupported) return { value: "unavailable", sub: "not on this platform" } as const;
|
|
457
|
+
// The sub-line carries the REMEDY, not a restatement of the value. Both
|
|
458
|
+
// states have the same one, and naming the command is the actionable half.
|
|
459
|
+
if (view.daemon === "not-installed")
|
|
460
|
+
return { value: "not installed", sub: "run failproofai config", tone: "warn" as const };
|
|
461
|
+
if (view.daemon === "unknown") return { value: "unknown", sub: "could not read it" } as const;
|
|
462
|
+
return { value: "stopped", sub: "run failproofai config", tone: "warn" as const };
|
|
463
|
+
})();
|
|
464
|
+
|
|
465
|
+
const nextCell = (() => {
|
|
466
|
+
if (!auto) return { value: "off", sub: "nothing scheduled" } as const;
|
|
467
|
+
if (sched?.nextDueAtMs == null) return { value: "pending", sub: "after the next scan" } as const;
|
|
468
|
+
return {
|
|
469
|
+
value: nowMs > 0 ? fmtUntil(sched.nextDueAtMs, nowMs) : "—",
|
|
470
|
+
sub: fmtAbsolute(sched.nextDueAtMs),
|
|
471
|
+
} as const;
|
|
472
|
+
})();
|
|
473
|
+
|
|
474
|
+
const lastCell = lastScan
|
|
475
|
+
? {
|
|
476
|
+
value: formatRelativeTime(Date.parse(lastScan.finishedAt)),
|
|
477
|
+
sub:
|
|
478
|
+
lastScan.sessionsScanned != null
|
|
479
|
+
? `${num(lastScan.sessionsScanned)} sessions`
|
|
480
|
+
: "scanned",
|
|
449
481
|
}
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
|
|
482
|
+
: ({ value: "none yet", sub: "run one below" } as const);
|
|
483
|
+
|
|
484
|
+
const findingsCell = (() => {
|
|
485
|
+
if (!lastScan) return { value: "—", sub: "no scan yet" } as const;
|
|
486
|
+
if (lastScan.findings == null) return { value: "—", sub: "unreadable" } as const;
|
|
487
|
+
return {
|
|
488
|
+
value: num(lastScan.findings),
|
|
489
|
+
sub: "this scan",
|
|
490
|
+
...(lastScan.findings > 0 ? { tone: "warn" as const } : {}),
|
|
453
491
|
};
|
|
454
|
-
}
|
|
492
|
+
})();
|
|
455
493
|
|
|
456
494
|
return (
|
|
457
495
|
<main className="report">
|
|
458
|
-
<section className="section
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
</p>
|
|
474
|
-
|
|
475
|
-
{loading ? (
|
|
476
|
-
<p style={MUTED}>Loading…</p>
|
|
477
|
-
) : error || !scheduled ? (
|
|
478
|
-
<p style={{ ...BODY, color: "var(--accent-pink)" }}>
|
|
479
|
-
Could not load settings. Refresh to try again.
|
|
480
|
-
</p>
|
|
496
|
+
<section className="section set-sec">
|
|
497
|
+
<div className="set-mast">
|
|
498
|
+
<h1 className="set-title">settings</h1>
|
|
499
|
+
<p className="set-lede">keeping watch, so you don't have to.</p>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
{loadError ? (
|
|
503
|
+
<div className="set-console">
|
|
504
|
+
<div className="set-cell">
|
|
505
|
+
<p className="set-warn">
|
|
506
|
+
could not read this machine's settings. check that the
|
|
507
|
+
dashboard can reach <code>~/.failproofai/config.json</code>.
|
|
508
|
+
</p>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
481
511
|
) : (
|
|
482
|
-
<
|
|
512
|
+
<div className="set-console">
|
|
513
|
+
<div className="set-stats">
|
|
514
|
+
<StatCell label="daemon" {...daemonCell} />
|
|
515
|
+
<StatCell label="next scan" {...nextCell} />
|
|
516
|
+
<StatCell label="last scan" {...lastCell} />
|
|
517
|
+
<StatCell label="findings" {...findingsCell} />
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<div className="set-cols">
|
|
521
|
+
<div className="set-cell set-config">
|
|
522
|
+
<h2 className="set-cell-title">scheduled audit</h2>
|
|
523
|
+
|
|
524
|
+
<div className="set-row set-row-main">
|
|
525
|
+
<Toggle
|
|
526
|
+
enabled={auto}
|
|
527
|
+
disabled={busy || view === null}
|
|
528
|
+
onChange={() => void onToggle()}
|
|
529
|
+
label={auto ? "turn off scheduled audits" : "turn on scheduled audits"}
|
|
530
|
+
/>
|
|
531
|
+
<div className="set-row-copy">
|
|
532
|
+
<span className="set-strong">
|
|
533
|
+
{auto ? "scanning on a schedule." : "scan on a schedule."}
|
|
534
|
+
</span>
|
|
535
|
+
<span className="set-dim">
|
|
536
|
+
{auto
|
|
537
|
+
? "you'll get an email only when a scan finds something."
|
|
538
|
+
: "off. nothing runs and nothing is sent."}
|
|
539
|
+
</span>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div className="set-row set-row-interval">
|
|
544
|
+
<span className="set-dim">scan every</span>
|
|
545
|
+
<input
|
|
546
|
+
type="number"
|
|
547
|
+
className="set-num"
|
|
548
|
+
min={MIN_INTERVAL_DAYS}
|
|
549
|
+
max={MAX_INTERVAL_DAYS}
|
|
550
|
+
value={intervalDays}
|
|
551
|
+
disabled={busy || view === null}
|
|
552
|
+
aria-label="days between scheduled scans"
|
|
553
|
+
onChange={(e) => setIntervalDays(Number(e.target.value))}
|
|
554
|
+
onBlur={(e) => {
|
|
555
|
+
const v = Number(e.target.value);
|
|
556
|
+
if (!Number.isFinite(v)) {
|
|
557
|
+
setIntervalDays(view?.intervalDays ?? 7);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (v !== view?.intervalDays) void commitInterval(v);
|
|
561
|
+
}}
|
|
562
|
+
/>
|
|
563
|
+
<span className="set-dim">days</span>
|
|
564
|
+
<span className="set-hint">
|
|
565
|
+
{MIN_INTERVAL_DAYS}–{MAX_INTERVAL_DAYS}
|
|
566
|
+
</span>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div className="set-rule" />
|
|
570
|
+
|
|
571
|
+
<div className="set-row set-row-identity">
|
|
572
|
+
{signedIn ? (
|
|
573
|
+
<>
|
|
574
|
+
<span className="set-dim">reports go to</span>
|
|
575
|
+
<span className="set-email">{signedIn.email}</span>
|
|
576
|
+
<button
|
|
577
|
+
type="button"
|
|
578
|
+
className="set-link"
|
|
579
|
+
disabled={busy}
|
|
580
|
+
onClick={() => void onSignOut()}
|
|
581
|
+
>
|
|
582
|
+
sign out
|
|
583
|
+
</button>
|
|
584
|
+
</>
|
|
585
|
+
) : auto ? (
|
|
586
|
+
// A control, not just a sentence. This state told the user
|
|
587
|
+
// to sign in and gave them nothing to sign in WITH: the
|
|
588
|
+
// dialog opened only from the off→on toggle and from
|
|
589
|
+
// `enable()`'s rejection path, so somebody whose session
|
|
590
|
+
// died while the timer stayed on had to guess that toggling
|
|
591
|
+
// off and back on was the way through. The CLI recovers
|
|
592
|
+
// from this in one command; the dashboard could not recover
|
|
593
|
+
// from it at all.
|
|
594
|
+
<>
|
|
595
|
+
<span className="set-warn">
|
|
596
|
+
signed out — scans continue, digests are paused.
|
|
597
|
+
</span>
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
className="set-link"
|
|
601
|
+
disabled={busy}
|
|
602
|
+
onClick={() => setAuthOpen(true)}
|
|
603
|
+
>
|
|
604
|
+
sign in to resume
|
|
605
|
+
</button>
|
|
606
|
+
</>
|
|
607
|
+
) : (
|
|
608
|
+
<span className="set-dim">
|
|
609
|
+
turning this on asks for an email, so there is somewhere to
|
|
610
|
+
send the report.
|
|
611
|
+
</span>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{auto && view && !daemonRunning && (
|
|
616
|
+
<p className="set-warn">
|
|
617
|
+
{daemonUnsupported
|
|
618
|
+
? "the background service isn't available on this platform, so scheduled scans can't run here. you can still run one now."
|
|
619
|
+
: view.daemon === "not-installed"
|
|
620
|
+
? "the background service isn't installed, so nothing will run on the timer. install it with `failproofai config`."
|
|
621
|
+
: "the background service is stopped, so nothing will run until it starts. repair it with `failproofai config`."}
|
|
622
|
+
</p>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{lastExitBad && (
|
|
626
|
+
<p className="set-warn">
|
|
627
|
+
the last scheduled scan exited {sched?.lastExitCode}. the next
|
|
628
|
+
one will still run.
|
|
629
|
+
</p>
|
|
630
|
+
)}
|
|
631
|
+
|
|
632
|
+
<div className="set-actions">
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
className="btn btn-press"
|
|
636
|
+
disabled={running}
|
|
637
|
+
onClick={() => void onRunNow()}
|
|
638
|
+
>
|
|
639
|
+
{running ? "[ scanning… ]" : "[ run a scan now ]"}
|
|
640
|
+
</button>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
<div className="set-cell set-how">
|
|
645
|
+
<h2 className="set-cell-title">how it works</h2>
|
|
646
|
+
<dl className="set-how-list">
|
|
647
|
+
{HOW_IT_WORKS.map((row) => (
|
|
648
|
+
<div className="set-how-row" key={row.label}>
|
|
649
|
+
<dt className="set-how-label">{row.label}</dt>
|
|
650
|
+
<dd className="set-how-body">{row.body}</dd>
|
|
651
|
+
</div>
|
|
652
|
+
))}
|
|
653
|
+
</dl>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
)}
|
|
658
|
+
|
|
659
|
+
{auto && nowMs > 0 && (
|
|
660
|
+
<ScheduleTape
|
|
661
|
+
lastRunAtMs={sched?.lastRunAtMs ?? null}
|
|
662
|
+
nextDueAtMs={sched?.nextDueAtMs ?? null}
|
|
663
|
+
now={nowMs}
|
|
664
|
+
/>
|
|
483
665
|
)}
|
|
484
666
|
</section>
|
|
667
|
+
|
|
668
|
+
<AuthDialog
|
|
669
|
+
open={authOpen}
|
|
670
|
+
source="settings_scheduled_audit"
|
|
671
|
+
headline="where should the report go?"
|
|
672
|
+
subhead="we'll send a one-time code to confirm."
|
|
673
|
+
onClose={() => setAuthOpen(false)}
|
|
674
|
+
onAuthed={(u) => void onAuthed(u)}
|
|
675
|
+
/>
|
|
485
676
|
</main>
|
|
486
677
|
);
|
|
487
678
|
}
|