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,29 +1,104 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Write side of the /settings
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Write side of the /settings scheduled-audit panel. Every write goes through
|
|
5
|
+
* `updateConfig` — never a raw file write — so `fp-config` stays the single
|
|
6
|
+
* writer of `config.json` and the dashboard can never disagree with what the
|
|
7
|
+
* CLI reads.
|
|
8
8
|
*
|
|
9
9
|
* ## CLI ⟷ dashboard parity
|
|
10
|
-
* - `setAutoAuditAction(enabled)`
|
|
11
|
-
* - `setAuditIntervalAction(days)`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* - `setAutoAuditAction(enabled)` ⟷ `[audit] auto`
|
|
11
|
+
* - `setAuditIntervalAction(days)` ⟷ `[audit] interval_days`
|
|
12
|
+
*
|
|
13
|
+
* Both go through the same `updateConfig` the CLI uses — `failproofai audit
|
|
14
|
+
* --schedule` / `--no-schedule` call it too — so a value set on either side is
|
|
15
|
+
* byte-identical. That is the whole mechanism behind "the two
|
|
16
|
+
* surfaces are always in sync": there is one file, one writer function, and no
|
|
17
|
+
* second copy of the state to drift.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { readConfig, updateConfig } from "@/src/hooks/fp-config";
|
|
21
|
+
import { readAuth, whoAmI } from "@/lib/auth/auth-store";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The outcome of trying to turn scheduling on.
|
|
25
|
+
*
|
|
26
|
+
* "Signed out" is RETURNED, not thrown, and that is the whole point of this
|
|
27
|
+
* type. Next masks a server action's thrown error before the browser sees it —
|
|
28
|
+
* the client gets an opaque digest, never the message — so a caller matching on
|
|
29
|
+
* the text works in development and silently degrades to a generic failure in
|
|
30
|
+
* production, which is exactly what happened: the page showed an address it had
|
|
31
|
+
* read from the local session file, the toggle took the signed-in path, and the
|
|
32
|
+
* user got "could not turn that on." with no way forward from that click.
|
|
33
|
+
*
|
|
34
|
+
* A returned discriminant survives the boundary, so the caller can open the
|
|
35
|
+
* sign-in dialog for the one failure that has an obvious next step.
|
|
36
|
+
*/
|
|
37
|
+
export type SetAutoAuditResult =
|
|
38
|
+
| { ok: true; auto: boolean }
|
|
39
|
+
| { ok: false; reason: "signed-out" }
|
|
40
|
+
/**
|
|
41
|
+
* The session is intact locally and the api-server could not be reached.
|
|
42
|
+
*
|
|
43
|
+
* Separated from `signed-out` because `whoAmI()` collapses them: it returns
|
|
44
|
+
* null for a 401 AND for every transport failure, so an offline machine, a
|
|
45
|
+
* proxy that blocks the host, and a genuinely expired token were one answer.
|
|
46
|
+
* The user was told "that sign-in expired" and handed a code prompt that
|
|
47
|
+
* cannot succeed either — the failure it names is not the failure they have,
|
|
48
|
+
* and following its advice costs them a real, working session.
|
|
49
|
+
*/
|
|
50
|
+
| { ok: false; reason: "unreachable" };
|
|
17
51
|
|
|
18
52
|
/**
|
|
19
53
|
* Turn the scheduled scan on or off.
|
|
20
54
|
*
|
|
55
|
+
* Turning it ON is refused without a session. Scheduling and mailing are ONE
|
|
56
|
+
* decision — the reason to put a scan on a timer is to be told what it found —
|
|
57
|
+
* so a machine with the timer set and nobody to tell is a switch that reads as
|
|
58
|
+
* on and produces nothing, discoverable only by noticing that no digest ever
|
|
59
|
+
* arrives. The caller signs the user in first and retries.
|
|
60
|
+
*
|
|
61
|
+
* `whoAmI()` asks the SERVER, so this refuses in a case the page cannot see: a
|
|
62
|
+
* session file that exists locally but whose refresh token the api-server has
|
|
63
|
+
* rejected. The local file is what the page reads to show "reports go to …", so
|
|
64
|
+
* the two disagree exactly when a session has expired or was minted against a
|
|
65
|
+
* different server — and that disagreement is the common case, not an edge one.
|
|
66
|
+
*
|
|
67
|
+
* Turning it OFF never checks. An expired session must not be able to trap
|
|
68
|
+
* somebody into keeping a feature they are trying to disable.
|
|
69
|
+
*
|
|
70
|
+
* Note this gates SETTING UP the timer, not the machine's ongoing work: a
|
|
71
|
+
* session that later expires leaves the timer running and the local scan
|
|
72
|
+
* working, and only the digest stops. See `report-harm.ts`.
|
|
73
|
+
*
|
|
21
74
|
* Returns the value actually stored (re-read), so an optimistic UI can confirm
|
|
22
75
|
* against the source of truth rather than assume its own guess landed.
|
|
23
76
|
*/
|
|
24
|
-
export async function setAutoAuditAction(enabled: boolean): Promise<
|
|
25
|
-
|
|
26
|
-
|
|
77
|
+
export async function setAutoAuditAction(enabled: boolean): Promise<SetAutoAuditResult> {
|
|
78
|
+
if (enabled) {
|
|
79
|
+
const who = await whoAmI();
|
|
80
|
+
if (!who) {
|
|
81
|
+
// `whoAmI()` deletes the session on a 401 and leaves it alone on a
|
|
82
|
+
// transport failure, so what is left ON DISK is the one thing that tells
|
|
83
|
+
// the two apart — no extra request, and nothing new that can fail.
|
|
84
|
+
const local = readAuth();
|
|
85
|
+
const stillWithinWindow = local && local.refresh_expires_at * 1000 > Date.now();
|
|
86
|
+
return { ok: false, reason: stillWithinWindow ? "unreachable" : "signed-out" };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Enabling stamps consent in the same write, because the `whoAmI()` above is
|
|
90
|
+
// exactly what makes this a consent record rather than a guess: a person was
|
|
91
|
+
// present, signed in, and looking at the panel that enumerates what gets
|
|
92
|
+
// sent. `reportHarm` gates sending on the stamp, not on `auto`, so a machine
|
|
93
|
+
// that inherited `auto` from a release where it meant "scan locally on a
|
|
94
|
+
// timer" mails nothing until somebody passes through here or the CLI.
|
|
95
|
+
//
|
|
96
|
+
// Disabling leaves the stamp alone. It is a record of something that did
|
|
97
|
+
// happen, and it grants nothing on its own — sending needs `auto` too.
|
|
98
|
+
const next = updateConfig({
|
|
99
|
+
audit: enabled ? { auto: true, reportsConsentedAt: Date.now() } : { auto: false },
|
|
100
|
+
});
|
|
101
|
+
return { ok: true, auto: next.audit.auto };
|
|
27
102
|
}
|
|
28
103
|
|
|
29
104
|
/**
|
|
@@ -31,14 +106,12 @@ export async function setAutoAuditAction(enabled: boolean): Promise<{ auto: bool
|
|
|
31
106
|
*
|
|
32
107
|
* The clamp lives in `fp-config.readIntervalDays` (1..90, with 0/negatives/
|
|
33
108
|
* fractions falling back to the default) and is DELIBERATELY not reimplemented
|
|
34
|
-
* here: we write the raw value and then RE-READ, so what
|
|
35
|
-
*
|
|
36
|
-
* hand-typed 3650 shows up
|
|
37
|
-
*
|
|
109
|
+
* here: we write the raw value and then RE-READ, so what comes back is exactly
|
|
110
|
+
* what the config decided to keep. Reflecting the re-read value is how a
|
|
111
|
+
* hand-typed 3650 shows up as the 90 the config actually enforces, with no
|
|
112
|
+
* second copy of the bounds to drift.
|
|
38
113
|
*/
|
|
39
114
|
export async function setAuditIntervalAction(days: number): Promise<{ intervalDays: number }> {
|
|
40
115
|
updateConfig({ audit: { intervalDays: days } });
|
|
41
|
-
// Re-read through readConfig so the returned value carries the config's own
|
|
42
|
-
// clamp, not the raw input.
|
|
43
116
|
return { intervalDays: readConfig().audit.intervalDays };
|
|
44
117
|
}
|
|
@@ -2,40 +2,30 @@
|
|
|
2
2
|
* GET /api/auth/status
|
|
3
3
|
*
|
|
4
4
|
* Returns the currently signed-in identity by reading the local
|
|
5
|
-
* `~/.failproofai/
|
|
5
|
+
* `~/.failproofai/audit/session.json` cache. No round-trip to the api-server — the
|
|
6
6
|
* file is the source of truth for who is signed in on this machine.
|
|
7
7
|
* This keeps the dashboard UI and the CLI consistent regardless of whether
|
|
8
8
|
* the api-server is reachable.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Reminders are gone: the machine now audits itself on a timer and mails a
|
|
11
|
+
* digest when it finds harm, so there is nothing to nudge anyone about. The
|
|
12
|
+
* scheduled-scan state lives in `getScheduledAuditAction`, which reads it from
|
|
13
|
+
* the config and the daemon rather than from here.
|
|
14
14
|
*/
|
|
15
15
|
import { NextResponse } from "next/server";
|
|
16
|
-
import { readAuth
|
|
16
|
+
import { readAuth } from "@/lib/auth/auth-store";
|
|
17
17
|
|
|
18
18
|
export const dynamic = "force-dynamic";
|
|
19
19
|
|
|
20
20
|
export async function GET(): Promise<NextResponse> {
|
|
21
21
|
const auth = readAuth();
|
|
22
22
|
if (!auth) {
|
|
23
|
-
return NextResponse.json({ authenticated: false
|
|
23
|
+
return NextResponse.json({ authenticated: false }, { status: 200 });
|
|
24
24
|
}
|
|
25
|
-
const reminderRaw = readReminder();
|
|
26
|
-
const reminder =
|
|
27
|
-
reminderRaw && reminderRaw.user_email === auth.user.email
|
|
28
|
-
? {
|
|
29
|
-
next_audit_at: reminderRaw.next_audit_at,
|
|
30
|
-
user_email: reminderRaw.user_email,
|
|
31
|
-
set_at: reminderRaw.set_at,
|
|
32
|
-
}
|
|
33
|
-
: null;
|
|
34
25
|
return NextResponse.json(
|
|
35
26
|
{
|
|
36
27
|
authenticated: true,
|
|
37
28
|
user: { id: auth.user.id, email: auth.user.email },
|
|
38
|
-
reminder,
|
|
39
29
|
},
|
|
40
30
|
{ status: 200 },
|
|
41
31
|
);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 02 StrengthsSection — what it's great at
|
|
11
11
|
* 03 QuirksSection — what slipped through
|
|
12
12
|
* 04 HowToImproveSection — install / configure
|
|
13
|
-
* 05 ComeBackBetterSection —
|
|
13
|
+
* 05 ComeBackBetterSection — spread the audit (invite)
|
|
14
14
|
*
|
|
15
15
|
* Empty / running states fall back to EmptyState and RunProgress.
|
|
16
16
|
*/
|
|
@@ -355,11 +355,7 @@ function MainReport({
|
|
|
355
355
|
projected={projected}
|
|
356
356
|
projectedGrade={projectedGrade}
|
|
357
357
|
/>
|
|
358
|
-
<ComeBackBetterSection
|
|
359
|
-
isRunning={isRunning}
|
|
360
|
-
onRerun={() => onRerun("return_section")}
|
|
361
|
-
score={score}
|
|
362
|
-
/>
|
|
358
|
+
<ComeBackBetterSection score={score} />
|
|
363
359
|
</div>
|
|
364
360
|
<ReportFooter cachedAt={cachedAt} />
|
|
365
361
|
</div>
|
|
@@ -1,335 +1,155 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Section 05 —
|
|
4
|
+
* Section 05 — SPREAD THE AUDIT.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* One job: get someone else to run this on their own machine. The scheduled-
|
|
7
|
+
* audit controls used to live here too and have moved to `/settings`, reachable
|
|
8
|
+
* from the gear in the header — they are machine configuration, and this is the
|
|
9
|
+
* end of a report. Mixing "here is what your agent did" with "here is how to
|
|
10
|
+
* configure a background service" made the last thing you read before leaving
|
|
11
|
+
* the page a settings form.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* • Unlock perks — share with N friends to unlock pro features for a
|
|
14
|
-
* month. UI only — invite tracking + entitlement is a follow-up; the
|
|
15
|
-
* button opens the same X share intent the poster uses.
|
|
16
|
-
*
|
|
17
|
-
* Re-audit moves out of this section: a small inline "or re-audit now"
|
|
18
|
-
* link sits under the reminder card so the affordance survives without
|
|
19
|
-
* dominating the layout.
|
|
13
|
+
* The AuthDialog is still here because inviting needs a sender identity to Cc.
|
|
14
|
+
* It is now the ONLY thing on this section that opens it, which is what makes
|
|
15
|
+
* the resume unambiguous — the bug this section used to have was a shared
|
|
16
|
+
* dialog whose success handler assumed which control had opened it.
|
|
20
17
|
*/
|
|
21
18
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
22
19
|
import { usePostHog } from "@/contexts/PostHogContext";
|
|
23
|
-
import { isAbortError } from "@/lib/fetch-with-timeout";
|
|
24
20
|
import { AuthDialog, type AuthedUser } from "./auth-dialog";
|
|
25
21
|
import { InviteDialog } from "./invite-dialog";
|
|
26
22
|
|
|
27
23
|
interface Props {
|
|
28
|
-
isRunning: boolean;
|
|
29
|
-
onRerun: () => void;
|
|
30
24
|
/** Current audit score (0–100), forwarded into the invite email body. */
|
|
31
25
|
score?: number;
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
const DEFAULT_REMINDER_DAYS = 7;
|
|
35
|
-
const REMINDER_OPTIONS = [3, 7, 14, 30] as const;
|
|
36
|
-
type Cadence = typeof REMINDER_OPTIONS[number];
|
|
37
|
-
|
|
38
28
|
const PERKS_PERK = "wanna know how your friends' agents score?";
|
|
39
29
|
|
|
40
|
-
// The AuthDialog is shared by the reminder and invite CTAs. The reminder path
|
|
41
|
-
// keeps the dialog's default copy; the invite path swaps in login-required
|
|
42
|
-
// copy. Content only — the auth flow is identical for both.
|
|
43
30
|
const INVITE_AUTH_COPY = {
|
|
44
31
|
headline: "Oops! Login required",
|
|
45
32
|
subhead: "What's your email?",
|
|
46
33
|
} as const;
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
| { kind: "unknown" }
|
|
50
|
-
| { kind: "anon" }
|
|
51
|
-
| { kind: "authed"; user: { id: string; email: string } };
|
|
52
|
-
|
|
53
|
-
interface Reminder {
|
|
54
|
-
next_audit_at: number;
|
|
55
|
-
user_email: string;
|
|
56
|
-
set_at: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function daysUntil(unixSecs: number): number {
|
|
60
|
-
const nowSecs = Math.floor(Date.now() / 1000);
|
|
61
|
-
return Math.max(0, Math.ceil((unixSecs - nowSecs) / 86400));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatNextAudit(unixSecs: number): string {
|
|
65
|
-
const d = new Date(unixSecs * 1000);
|
|
66
|
-
return d.toLocaleDateString(undefined, {
|
|
67
|
-
weekday: "short",
|
|
68
|
-
month: "short",
|
|
69
|
-
day: "numeric",
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function ComeBackBetterSection({ isRunning, onRerun, score }: Props) {
|
|
35
|
+
export function ComeBackBetterSection({ score }: Props) {
|
|
74
36
|
const { capture } = usePostHog();
|
|
75
|
-
const [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
// Preserve current UI state on transient failures (5xx, network blips).
|
|
91
|
-
// Downgrading to anon on every error would clear a valid reminder mid-
|
|
92
|
-
// session on a single failed poll, forcing an unnecessary auth prompt.
|
|
93
|
-
// Only fall through to anon on the very first probe (still "unknown")
|
|
94
|
-
// so the cadence buttons unlock even if the server is unreachable.
|
|
95
|
-
const fallbackToAnonOnError = () => {
|
|
96
|
-
setAuthStatus((prev) => (prev.kind === "unknown" ? { kind: "anon" } : prev));
|
|
97
|
-
};
|
|
98
|
-
try {
|
|
99
|
-
const res = await fetch("/api/auth/status", { cache: "no-store" });
|
|
100
|
-
if (!res.ok) {
|
|
101
|
-
fallbackToAnonOnError();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const body = (await res.json()) as {
|
|
105
|
-
authenticated?: boolean;
|
|
106
|
-
user?: { id: string; email: string };
|
|
107
|
-
reminder?: Reminder | null;
|
|
108
|
-
};
|
|
109
|
-
if (body.authenticated && body.user) {
|
|
110
|
-
setAuthStatus({ kind: "authed", user: body.user });
|
|
111
|
-
setReminder(body.reminder ?? null);
|
|
112
|
-
} else {
|
|
113
|
-
setAuthStatus({ kind: "anon" });
|
|
114
|
-
setReminder(null);
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
fallbackToAnonOnError();
|
|
118
|
-
}
|
|
119
|
-
}, []);
|
|
37
|
+
const [signedIn, setSignedIn] = useState<{ id: string; email: string } | null>(null);
|
|
38
|
+
/**
|
|
39
|
+
* Whether the sign-in probe below has come back yet.
|
|
40
|
+
*
|
|
41
|
+
* `signedIn` starts null and null also means "signed out", so on its own it
|
|
42
|
+
* cannot say whether the answer has arrived — and the impression event fires
|
|
43
|
+
* on the first commit, which is always before the fetch resolves. It
|
|
44
|
+
* therefore reported `signed_in: false` for every view ever recorded,
|
|
45
|
+
* including a signed-in one. A separate flag restores the tri-state the
|
|
46
|
+
* previous version of this section carried for the same reason.
|
|
47
|
+
*/
|
|
48
|
+
const [probed, setProbed] = useState(false);
|
|
49
|
+
const [authOpen, setAuthOpen] = useState(false);
|
|
50
|
+
const [inviteOpen, setInviteOpen] = useState(false);
|
|
51
|
+
const shownRef = useRef(false);
|
|
120
52
|
|
|
121
53
|
useEffect(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
54
|
+
// Cancellation guard rather than a bare fire-and-forget: the probe outlives
|
|
55
|
+
// a fast unmount otherwise, and setting state on a gone component is the
|
|
56
|
+
// kind of warning people learn to scroll past.
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
(async () => {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch("/api/auth/status", { cache: "no-store" });
|
|
61
|
+
if (!res.ok || cancelled) return;
|
|
62
|
+
const body = (await res.json()) as {
|
|
63
|
+
authenticated?: boolean;
|
|
64
|
+
user?: { id: string; email: string };
|
|
65
|
+
};
|
|
66
|
+
if (!cancelled) setSignedIn(body.authenticated && body.user ? body.user : null);
|
|
67
|
+
} catch {
|
|
68
|
+
// Leave whatever we last knew. A failed probe is not evidence of a
|
|
69
|
+
// signed-out user, and downgrading on one would prompt for a login the
|
|
70
|
+
// person already completed.
|
|
71
|
+
} finally {
|
|
72
|
+
// In `finally`, so a route that 404s or a fetch that throws still
|
|
73
|
+
// releases the impression event. A failed probe genuinely does not know
|
|
74
|
+
// whether anyone is signed in, and never reporting the view at all is a
|
|
75
|
+
// worse answer than reporting the one it has.
|
|
76
|
+
if (!cancelled) setProbed(true);
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
134
79
|
return () => {
|
|
135
|
-
|
|
136
|
-
document.removeEventListener("visibilitychange", onVisibility);
|
|
80
|
+
cancelled = true;
|
|
137
81
|
};
|
|
138
|
-
}, [
|
|
82
|
+
}, []);
|
|
139
83
|
|
|
140
84
|
useEffect(() => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
auth_state: authStatus.kind,
|
|
146
|
-
has_existing_reminder: reminder !== null,
|
|
147
|
-
source: "come_back_better_section",
|
|
148
|
-
});
|
|
149
|
-
}, [authStatus, capture, reminder]);
|
|
150
|
-
|
|
151
|
-
const persistReminder = useCallback(
|
|
152
|
-
async (inDays: number): Promise<Reminder | null> => {
|
|
153
|
-
const controller = new AbortController();
|
|
154
|
-
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
155
|
-
try {
|
|
156
|
-
setReminderBusy(true);
|
|
157
|
-
const res = await fetch("/api/auth/reminder", {
|
|
158
|
-
method: "POST",
|
|
159
|
-
headers: { "content-type": "application/json" },
|
|
160
|
-
body: JSON.stringify({ in_days: inDays }),
|
|
161
|
-
signal: controller.signal,
|
|
162
|
-
});
|
|
163
|
-
if (!res.ok) {
|
|
164
|
-
if (res.status === 401) {
|
|
165
|
-
setAuthStatus({ kind: "anon" });
|
|
166
|
-
setReminder(null);
|
|
167
|
-
}
|
|
168
|
-
capture("audit_reminder_saved", {
|
|
169
|
-
status: `http_${res.status}`,
|
|
170
|
-
source: "come_back_better_section",
|
|
171
|
-
cadence_days: inDays,
|
|
172
|
-
});
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
const body = (await res.json()) as { reminder?: Reminder };
|
|
176
|
-
capture("audit_reminder_saved", {
|
|
177
|
-
status: body.reminder ? "success" : "empty",
|
|
178
|
-
source: "come_back_better_section",
|
|
179
|
-
cadence_days: inDays,
|
|
180
|
-
});
|
|
181
|
-
return body.reminder ?? null;
|
|
182
|
-
} catch (err) {
|
|
183
|
-
const kind = isAbortError(err) ? "timeout" : "error";
|
|
184
|
-
capture("audit_reminder_saved", {
|
|
185
|
-
status: kind,
|
|
186
|
-
source: "come_back_better_section",
|
|
187
|
-
cadence_days: inDays,
|
|
188
|
-
});
|
|
189
|
-
return null;
|
|
190
|
-
} finally {
|
|
191
|
-
clearTimeout(timer);
|
|
192
|
-
setReminderBusy(false);
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
[capture],
|
|
196
|
-
);
|
|
85
|
+
if (!probed || shownRef.current) return;
|
|
86
|
+
shownRef.current = true;
|
|
87
|
+
capture("audit_share_section_shown", { signed_in: signedIn !== null });
|
|
88
|
+
}, [capture, probed, signedIn]);
|
|
197
89
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (authStatus.kind === "authed") {
|
|
208
|
-
const saved = await persistReminder(next);
|
|
209
|
-
if (saved) setReminder(saved);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (authStatus.kind === "anon") {
|
|
213
|
-
setAuthCopy({}); // reminder context → keep the dialog's default copy
|
|
214
|
-
setDialogOpen(true);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
[authStatus, capture, persistReminder, reminder],
|
|
218
|
-
);
|
|
90
|
+
const handleInvite = useCallback(() => {
|
|
91
|
+
capture("audit_perks_invite_clicked", { signed_in: signedIn !== null });
|
|
92
|
+
// Unauthed users sign in first, so the invite has a sender to Cc.
|
|
93
|
+
if (!signedIn) {
|
|
94
|
+
setAuthOpen(true);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setInviteOpen(true);
|
|
98
|
+
}, [capture, signedIn]);
|
|
219
99
|
|
|
220
100
|
const handleAuthed = useCallback(
|
|
221
101
|
async (user: AuthedUser) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (saved) setReminder(saved);
|
|
102
|
+
setSignedIn(user);
|
|
103
|
+
setAuthOpen(false);
|
|
104
|
+
capture("audit_auth_completed", { source: "share_section" });
|
|
105
|
+
// Resume the one thing that could have opened the dialog.
|
|
106
|
+
setInviteOpen(true);
|
|
228
107
|
},
|
|
229
|
-
[
|
|
108
|
+
[capture],
|
|
230
109
|
);
|
|
231
110
|
|
|
232
|
-
const handleInvite = useCallback(() => {
|
|
233
|
-
capture("audit_perks_invite_clicked", {
|
|
234
|
-
source: "come_back_better_section",
|
|
235
|
-
auth_state: authStatus.kind,
|
|
236
|
-
});
|
|
237
|
-
// Unauthed users go through the AuthDialog first so we have a sender
|
|
238
|
-
// identity to Cc on the invite email.
|
|
239
|
-
if (authStatus.kind !== "authed") {
|
|
240
|
-
setAuthCopy(INVITE_AUTH_COPY); // invite context → "Oops! Login required"
|
|
241
|
-
setDialogOpen(true);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
setInviteDialogOpen(true);
|
|
245
|
-
}, [authStatus.kind, capture]);
|
|
246
|
-
|
|
247
|
-
const handleRerunInline = useCallback(() => {
|
|
248
|
-
if (isRunning) return;
|
|
249
|
-
onRerun();
|
|
250
|
-
}, [isRunning, onRerun]);
|
|
251
|
-
|
|
252
|
-
const days = reminder ? daysUntil(reminder.next_audit_at) : 0;
|
|
253
|
-
|
|
254
111
|
return (
|
|
255
|
-
<section className="audit-sec" data-screen-label="05
|
|
112
|
+
<section className="audit-sec" data-screen-label="05 Spread the audit">
|
|
256
113
|
<div className="audit-sec-head">
|
|
257
|
-
<
|
|
258
|
-
<span className="ix">05</span>
|
|
259
|
-
</span>
|
|
260
|
-
</div>
|
|
261
|
-
<h2 className="audit-sec-title">build the habit</h2>
|
|
262
|
-
|
|
263
|
-
<div className="cbb-grid">
|
|
264
|
-
{/* Reminder card */}
|
|
265
|
-
<div className="cbb-card">
|
|
266
|
-
<div className="cbb-card-title">set a reminder</div>
|
|
267
|
-
<div className="cbb-card-sub">
|
|
268
|
-
{reminder
|
|
269
|
-
? `next audit set for ${formatNextAudit(reminder.next_audit_at)} · in ${days} day${days === 1 ? "" : "s"}.`
|
|
270
|
-
: "we'll nudge you when your next audit is due. pick the cadence:"}
|
|
271
|
-
</div>
|
|
272
|
-
<div className="cadence-row">
|
|
273
|
-
{REMINDER_OPTIONS.map((d) => (
|
|
274
|
-
<button
|
|
275
|
-
key={d}
|
|
276
|
-
type="button"
|
|
277
|
-
className={`cadence-btn${cadence === d ? " on" : ""}`}
|
|
278
|
-
disabled={reminderBusy || authStatus.kind === "unknown"}
|
|
279
|
-
onClick={() => void handleCadenceClick(d)}
|
|
280
|
-
>
|
|
281
|
-
{d}d
|
|
282
|
-
</button>
|
|
283
|
-
))}
|
|
284
|
-
</div>
|
|
285
|
-
<button
|
|
286
|
-
type="button"
|
|
287
|
-
className="cbb-link"
|
|
288
|
-
disabled={isRunning}
|
|
289
|
-
onClick={handleRerunInline}
|
|
290
|
-
>
|
|
291
|
-
{isRunning ? "scanning…" : "or re-audit now →"}
|
|
292
|
-
</button>
|
|
114
|
+
<div className="audit-sec-eyebrow">
|
|
115
|
+
<span className="ix">05</span> share
|
|
293
116
|
</div>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
117
|
+
</div>
|
|
118
|
+
<h2 className="audit-sec-title">spread the audit</h2>
|
|
119
|
+
|
|
120
|
+
<div className="cbb-card share-card">
|
|
121
|
+
<div className="cbb-card-title">Share with friends</div>
|
|
122
|
+
<div className="cbb-card-sub">{PERKS_PERK}</div>
|
|
123
|
+
<button type="button" className="invite-btn" onClick={handleInvite}>
|
|
124
|
+
invite a friend
|
|
125
|
+
</button>
|
|
126
|
+
<div className="cbb-foot">
|
|
127
|
+
{"// invites are sent from failproof.ai, Cc'd to you, with a link to run their own audit."}
|
|
305
128
|
</div>
|
|
306
129
|
</div>
|
|
307
130
|
|
|
308
131
|
<InviteDialog
|
|
309
|
-
open={
|
|
132
|
+
open={inviteOpen}
|
|
310
133
|
source="come_back_better_section"
|
|
311
134
|
score={score}
|
|
312
|
-
onClose={() =>
|
|
135
|
+
onClose={() => setInviteOpen(false)}
|
|
313
136
|
onUnauthorized={() => {
|
|
314
|
-
// Session expired between probe and submit
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
137
|
+
// Session expired between the probe and the submit. Bounce through
|
|
138
|
+
// the dialog; success reopens the invite, since that is the only
|
|
139
|
+
// thing it can be resuming.
|
|
140
|
+
setInviteOpen(false);
|
|
141
|
+
setSignedIn(null);
|
|
142
|
+
setAuthOpen(true);
|
|
320
143
|
}}
|
|
321
144
|
/>
|
|
322
145
|
|
|
323
146
|
<AuthDialog
|
|
324
|
-
open={
|
|
325
|
-
source="
|
|
326
|
-
headline={
|
|
327
|
-
subhead={
|
|
328
|
-
onClose={() =>
|
|
329
|
-
onAuthed={(u) =>
|
|
330
|
-
setDialogOpen(false);
|
|
331
|
-
void handleAuthed(u);
|
|
332
|
-
}}
|
|
147
|
+
open={authOpen}
|
|
148
|
+
source="share_section"
|
|
149
|
+
headline={INVITE_AUTH_COPY.headline}
|
|
150
|
+
subhead={INVITE_AUTH_COPY.subhead}
|
|
151
|
+
onClose={() => setAuthOpen(false)}
|
|
152
|
+
onAuthed={(u) => void handleAuthed(u)}
|
|
333
153
|
/>
|
|
334
154
|
</section>
|
|
335
155
|
);
|