failproofai 0.0.11-beta.8 → 0.0.11-beta.9
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 +1 -0
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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.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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +2 -2
- 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/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +7 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +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 +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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/build-manifest.json +3 -3
- 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 +3 -3
- 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/build-manifest.json +3 -3
- 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 +3 -3
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- 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-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1r1h8v9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uatkiv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1y6gxxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_audit_invite_route_actions_0-2n5sy.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1cd25c7._.js → [root-of-the-server]__0e4-6d8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1d4gx_t._.js → [root-of-the-server]__0e446gb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ehe24g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-scthx._.js → [root-of-the-server]__0f62vu9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l13qf2._.js → [root-of-the-server]__0k65l27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__15i0juc._.js → [root-of-the-server]__0wprfyc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0989_dx._.js → [root-of-the-server]__1e-x7j4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1uvfwgr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0il3fl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +21 -21
- 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/node_modules_html-to-image_es_index_0y4a-0q.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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 +10 -10
- package/.next/standalone/.next/static/chunks/{1nd1e30h8s_mc.js → 07_d165p5h5ys.js} +1 -1
- package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +1 -0
- package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +1 -0
- package/.next/standalone/.next/static/chunks/168k-8z6k7e8z.css +1 -0
- package/.next/standalone/.next/static/chunks/{24z-bgbisv379.js → 1kvadxkgnapyj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29gs4efgi3hme.js → 277oc363p56n6.js} +2 -2
- package/.next/standalone/.next/static/chunks/28mkxkl_d91-l.js +1 -0
- package/.next/standalone/.next/static/chunks/28x7jvo3kxd3u.js +41 -0
- package/.next/standalone/.next/static/chunks/29nrs5xs9c4hx.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mni177pnjx6u.js → 29tg7deqmq32l.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3w8d8k_dca5rp.js → 2h0dkzyy0vocp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0j969hb6nujdf.js → 2z42u62k-8-_q.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1m2yj97j7f_km.js → 3nj6g3xu9uy78.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d49wc5zca0u1.js → 3ty6dhcuogout.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-00qy7zfa7m--m.js → turbopack-3lrm4f20fz89b.js} +1 -1
- package/.next/standalone/app/api/audit/invite/route.ts +183 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +30 -62
- package/.next/standalone/app/audit/_components/audit-poster.tsx +322 -0
- package/.next/standalone/app/audit/_components/auth-dialog.tsx +21 -49
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +316 -0
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +187 -0
- package/.next/standalone/app/audit/_components/invite-dialog.tsx +227 -0
- package/.next/standalone/app/audit/_components/quirks-section.tsx +75 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +23 -22
- package/.next/standalone/app/audit/_components/sigil.tsx +9 -66
- package/.next/standalone/app/audit/_components/strengths-section.tsx +20 -32
- package/.next/standalone/app/audit/audit-styles.css +781 -1784
- package/.next/standalone/app/components/sessions-list.tsx +77 -80
- package/.next/standalone/app/globals.css +214 -32
- package/.next/standalone/app/layout.tsx +1 -10
- package/.next/standalone/app/policies/hooks-client.tsx +12 -4
- package/.next/standalone/app/project/[name]/page.tsx +23 -79
- package/.next/standalone/app/projects/page.tsx +14 -23
- package/.next/standalone/assets/audit/poster-styles.css +1 -1
- package/.next/standalone/assets/audit/styles.css +11 -11
- package/.next/standalone/components/navbar.tsx +2 -37
- package/.next/standalone/lib/auth/api-server-client.ts +25 -0
- package/.next/standalone/node_modules/@next/env/package.json +2 -2
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +15 -15
- package/.next/standalone/package.json +15 -12
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +2 -2
- package/lib/auth/api-server-client.ts +25 -0
- package/package.json +15 -12
- package/src/audit/social-proof.ts +34 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07tgnzi._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oeun7z._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12pit4m._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13ra2jq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b9z5-i._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ixjiy8._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1-1804f._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00jkjmt._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013du6r._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e85wxv._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gfxvb1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hlrq6y._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ihxdo5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1vvfde2._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html2canvas_dist_html2canvas_esm_1k58rb_.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html2canvas_dist_html2canvas_esm_1n-0xws.js +0 -3
- package/.next/standalone/.next/static/chunks/09ueq8s1as8xs.css +0 -2
- package/.next/standalone/.next/static/chunks/0qassxjx1ef04.js +0 -1
- package/.next/standalone/.next/static/chunks/0qxb5czqxe-vu.js +0 -1
- package/.next/standalone/.next/static/chunks/1dh06515j265n.js +0 -41
- package/.next/standalone/.next/static/chunks/2so39wg7mjbi7.js +0 -1
- package/.next/standalone/.next/static/chunks/3gti1qdk5epqn.js +0 -1
- package/.next/standalone/.next/static/chunks/3wycox197ouus.css +0 -1
- package/.next/standalone/app/audit/_components/findings-section.tsx +0 -135
- package/.next/standalone/app/audit/_components/identity-section.tsx +0 -126
- package/.next/standalone/app/audit/_components/policies-section.tsx +0 -194
- package/.next/standalone/app/audit/_components/return-section.tsx +0 -416
- package/.next/standalone/app/audit/_components/score-section.tsx +0 -179
- package/.next/standalone/app/audit/_components/share-dock.tsx +0 -265
- package/.next/standalone/app/audit/_components/show-off-cta.tsx +0 -135
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section 05 — COME BACK BETTER. "build the habit."
|
|
5
|
+
*
|
|
6
|
+
* Two side-by-side cards:
|
|
7
|
+
*
|
|
8
|
+
* • Reminder — set a reminder cadence (3d / 7d / 14d / 30d). The cadence
|
|
9
|
+
* selection persists through /api/auth/reminder. Anon users get the
|
|
10
|
+
* AuthDialog first; authed-with-existing-reminder users see the next
|
|
11
|
+
* audit date and can reset.
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
22
|
+
import { usePostHog } from "@/contexts/PostHogContext";
|
|
23
|
+
import { isAbortError } from "@/lib/fetch-with-timeout";
|
|
24
|
+
import { AuthDialog, type AuthedUser } from "./auth-dialog";
|
|
25
|
+
import { InviteDialog } from "./invite-dialog";
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
isRunning: boolean;
|
|
29
|
+
onRerun: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_REMINDER_DAYS = 7;
|
|
33
|
+
const REMINDER_OPTIONS = [3, 7, 14, 30] as const;
|
|
34
|
+
type Cadence = typeof REMINDER_OPTIONS[number];
|
|
35
|
+
|
|
36
|
+
const PERKS_PERK = "share with 3 friends → unlock pro features for a month.";
|
|
37
|
+
|
|
38
|
+
type AuthStatus =
|
|
39
|
+
| { kind: "unknown" }
|
|
40
|
+
| { kind: "anon" }
|
|
41
|
+
| { kind: "authed"; user: { id: string; email: string } };
|
|
42
|
+
|
|
43
|
+
interface Reminder {
|
|
44
|
+
next_audit_at: number;
|
|
45
|
+
user_email: string;
|
|
46
|
+
set_at: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function daysUntil(unixSecs: number): number {
|
|
50
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
51
|
+
return Math.max(0, Math.ceil((unixSecs - nowSecs) / 86400));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatNextAudit(unixSecs: number): string {
|
|
55
|
+
const d = new Date(unixSecs * 1000);
|
|
56
|
+
return d.toLocaleDateString(undefined, {
|
|
57
|
+
weekday: "short",
|
|
58
|
+
month: "short",
|
|
59
|
+
day: "numeric",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ComeBackBetterSection({ isRunning, onRerun }: Props) {
|
|
64
|
+
const { capture } = usePostHog();
|
|
65
|
+
const [authStatus, setAuthStatus] = useState<AuthStatus>({ kind: "unknown" });
|
|
66
|
+
const [reminder, setReminder] = useState<Reminder | null>(null);
|
|
67
|
+
const [cadence, setCadence] = useState<Cadence>(DEFAULT_REMINDER_DAYS);
|
|
68
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
69
|
+
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
|
70
|
+
const [reminderBusy, setReminderBusy] = useState(false);
|
|
71
|
+
const ctaShownRef = useRef(false);
|
|
72
|
+
const lastRefreshAtRef = useRef(0);
|
|
73
|
+
|
|
74
|
+
const refreshStatus = useCallback(async () => {
|
|
75
|
+
lastRefreshAtRef.current = Date.now();
|
|
76
|
+
// Preserve current UI state on transient failures (5xx, network blips).
|
|
77
|
+
// Downgrading to anon on every error would clear a valid reminder mid-
|
|
78
|
+
// session on a single failed poll, forcing an unnecessary auth prompt.
|
|
79
|
+
// Only fall through to anon on the very first probe (still "unknown")
|
|
80
|
+
// so the cadence buttons unlock even if the server is unreachable.
|
|
81
|
+
const fallbackToAnonOnError = () => {
|
|
82
|
+
setAuthStatus((prev) => (prev.kind === "unknown" ? { kind: "anon" } : prev));
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch("/api/auth/status", { cache: "no-store" });
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
fallbackToAnonOnError();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const body = (await res.json()) as {
|
|
91
|
+
authenticated?: boolean;
|
|
92
|
+
user?: { id: string; email: string };
|
|
93
|
+
reminder?: Reminder | null;
|
|
94
|
+
};
|
|
95
|
+
if (body.authenticated && body.user) {
|
|
96
|
+
setAuthStatus({ kind: "authed", user: body.user });
|
|
97
|
+
setReminder(body.reminder ?? null);
|
|
98
|
+
} else {
|
|
99
|
+
setAuthStatus({ kind: "anon" });
|
|
100
|
+
setReminder(null);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
fallbackToAnonOnError();
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
void refreshStatus();
|
|
109
|
+
const REFRESH_MIN_INTERVAL_MS = 5_000;
|
|
110
|
+
const maybeRefresh = () => {
|
|
111
|
+
if (Date.now() - lastRefreshAtRef.current < REFRESH_MIN_INTERVAL_MS) return;
|
|
112
|
+
void refreshStatus();
|
|
113
|
+
};
|
|
114
|
+
const onFocus = () => maybeRefresh();
|
|
115
|
+
const onVisibility = () => {
|
|
116
|
+
if (document.visibilityState === "visible") maybeRefresh();
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener("focus", onFocus);
|
|
119
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
120
|
+
return () => {
|
|
121
|
+
window.removeEventListener("focus", onFocus);
|
|
122
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
123
|
+
};
|
|
124
|
+
}, [refreshStatus]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (ctaShownRef.current) return;
|
|
128
|
+
if (authStatus.kind === "unknown") return;
|
|
129
|
+
ctaShownRef.current = true;
|
|
130
|
+
capture("audit_reminder_cta_shown", {
|
|
131
|
+
auth_state: authStatus.kind,
|
|
132
|
+
has_existing_reminder: reminder !== null,
|
|
133
|
+
source: "come_back_better_section",
|
|
134
|
+
});
|
|
135
|
+
}, [authStatus, capture, reminder]);
|
|
136
|
+
|
|
137
|
+
const persistReminder = useCallback(
|
|
138
|
+
async (inDays: number): Promise<Reminder | null> => {
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
141
|
+
try {
|
|
142
|
+
setReminderBusy(true);
|
|
143
|
+
const res = await fetch("/api/auth/reminder", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "content-type": "application/json" },
|
|
146
|
+
body: JSON.stringify({ in_days: inDays }),
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
if (res.status === 401) {
|
|
151
|
+
setAuthStatus({ kind: "anon" });
|
|
152
|
+
setReminder(null);
|
|
153
|
+
}
|
|
154
|
+
capture("audit_reminder_saved", {
|
|
155
|
+
status: `http_${res.status}`,
|
|
156
|
+
source: "come_back_better_section",
|
|
157
|
+
cadence_days: inDays,
|
|
158
|
+
});
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const body = (await res.json()) as { reminder?: Reminder };
|
|
162
|
+
capture("audit_reminder_saved", {
|
|
163
|
+
status: body.reminder ? "success" : "empty",
|
|
164
|
+
source: "come_back_better_section",
|
|
165
|
+
cadence_days: inDays,
|
|
166
|
+
});
|
|
167
|
+
return body.reminder ?? null;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const kind = isAbortError(err) ? "timeout" : "error";
|
|
170
|
+
capture("audit_reminder_saved", {
|
|
171
|
+
status: kind,
|
|
172
|
+
source: "come_back_better_section",
|
|
173
|
+
cadence_days: inDays,
|
|
174
|
+
});
|
|
175
|
+
return null;
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
setReminderBusy(false);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[capture],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const handleCadenceClick = useCallback(
|
|
185
|
+
async (next: Cadence) => {
|
|
186
|
+
setCadence(next);
|
|
187
|
+
capture("audit_reminder_cta_clicked", {
|
|
188
|
+
auth_state: authStatus.kind,
|
|
189
|
+
has_existing_reminder: reminder !== null,
|
|
190
|
+
cadence_days: next,
|
|
191
|
+
source: "come_back_better_section",
|
|
192
|
+
});
|
|
193
|
+
if (authStatus.kind === "authed") {
|
|
194
|
+
const saved = await persistReminder(next);
|
|
195
|
+
if (saved) setReminder(saved);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (authStatus.kind === "anon") {
|
|
199
|
+
setDialogOpen(true);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[authStatus, capture, persistReminder, reminder],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleAuthed = useCallback(
|
|
206
|
+
async (user: AuthedUser) => {
|
|
207
|
+
setAuthStatus({ kind: "authed", user });
|
|
208
|
+
capture("audit_auth_completed", {
|
|
209
|
+
source: "come_back_better_section",
|
|
210
|
+
});
|
|
211
|
+
const saved = await persistReminder(cadence);
|
|
212
|
+
if (saved) setReminder(saved);
|
|
213
|
+
},
|
|
214
|
+
[cadence, capture, persistReminder],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const handleInvite = useCallback(() => {
|
|
218
|
+
capture("audit_perks_invite_clicked", {
|
|
219
|
+
source: "come_back_better_section",
|
|
220
|
+
auth_state: authStatus.kind,
|
|
221
|
+
});
|
|
222
|
+
// Unauthed users go through the AuthDialog first so we have a sender
|
|
223
|
+
// identity to Cc on the invite email.
|
|
224
|
+
if (authStatus.kind !== "authed") {
|
|
225
|
+
setDialogOpen(true);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setInviteDialogOpen(true);
|
|
229
|
+
}, [authStatus.kind, capture]);
|
|
230
|
+
|
|
231
|
+
const handleRerunInline = useCallback(() => {
|
|
232
|
+
if (isRunning) return;
|
|
233
|
+
onRerun();
|
|
234
|
+
}, [isRunning, onRerun]);
|
|
235
|
+
|
|
236
|
+
const days = reminder ? daysUntil(reminder.next_audit_at) : 0;
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<section className="audit-sec" data-screen-label="05 Come back better">
|
|
240
|
+
<div className="audit-sec-head">
|
|
241
|
+
<span className="audit-sec-eyebrow">
|
|
242
|
+
<span className="ix">05</span>{"// come back better"}
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
<h2 className="audit-sec-title">build the habit</h2>
|
|
246
|
+
|
|
247
|
+
<div className="cbb-grid">
|
|
248
|
+
{/* Reminder card */}
|
|
249
|
+
<div className="cbb-card">
|
|
250
|
+
<div className="cbb-card-title">set a reminder</div>
|
|
251
|
+
<div className="cbb-card-sub">
|
|
252
|
+
{reminder
|
|
253
|
+
? `next audit set for ${formatNextAudit(reminder.next_audit_at)} · in ${days} day${days === 1 ? "" : "s"}.`
|
|
254
|
+
: "we'll nudge you when your next audit is due. pick the cadence:"}
|
|
255
|
+
</div>
|
|
256
|
+
<div className="cadence-row">
|
|
257
|
+
{REMINDER_OPTIONS.map((d) => (
|
|
258
|
+
<button
|
|
259
|
+
key={d}
|
|
260
|
+
type="button"
|
|
261
|
+
className={`cadence-btn${cadence === d ? " on" : ""}`}
|
|
262
|
+
disabled={reminderBusy || authStatus.kind === "unknown"}
|
|
263
|
+
onClick={() => void handleCadenceClick(d)}
|
|
264
|
+
>
|
|
265
|
+
{d}d
|
|
266
|
+
</button>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
className="cbb-link"
|
|
272
|
+
disabled={isRunning}
|
|
273
|
+
onClick={handleRerunInline}
|
|
274
|
+
>
|
|
275
|
+
{isRunning ? "scanning…" : "or re-audit now →"}
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Perks card */}
|
|
280
|
+
<div className="cbb-card">
|
|
281
|
+
<div className="cbb-card-title">unlock failproof perks</div>
|
|
282
|
+
<div className="cbb-card-sub">{PERKS_PERK}</div>
|
|
283
|
+
<button type="button" className="invite-btn" onClick={handleInvite}>
|
|
284
|
+
invite a friend
|
|
285
|
+
</button>
|
|
286
|
+
<div className="cbb-foot">
|
|
287
|
+
{"// invites are sent from failproof.ai, Cc'd to you, with a link to run their own audit."}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<InviteDialog
|
|
293
|
+
open={inviteDialogOpen}
|
|
294
|
+
source="come_back_better_section"
|
|
295
|
+
onClose={() => setInviteDialogOpen(false)}
|
|
296
|
+
onUnauthorized={() => {
|
|
297
|
+
// Session expired between probe and submit — flip back to anon
|
|
298
|
+
// and bounce through the AuthDialog so the user re-auths.
|
|
299
|
+
setAuthStatus({ kind: "anon" });
|
|
300
|
+
setReminder(null);
|
|
301
|
+
setDialogOpen(true);
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
|
|
305
|
+
<AuthDialog
|
|
306
|
+
open={dialogOpen}
|
|
307
|
+
source="return_section"
|
|
308
|
+
onClose={() => setDialogOpen(false)}
|
|
309
|
+
onAuthed={(u) => {
|
|
310
|
+
setDialogOpen(false);
|
|
311
|
+
void handleAuthed(u);
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
</section>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section 04 — HOW TO IMPROVE. Calm row list, one per prescribed
|
|
5
|
+
* policy:
|
|
6
|
+
*
|
|
7
|
+
* <policy-name> $ failproofai policy add <slug> [📋]
|
|
8
|
+
* <one-line explanation>
|
|
9
|
+
*
|
|
10
|
+
* A single "install all" button at the section header copies the
|
|
11
|
+
* combined install command for every prescribed policy.
|
|
12
|
+
*/
|
|
13
|
+
import React, { useMemo, useState } from "react";
|
|
14
|
+
import type { AuditResult } from "@/src/audit/types";
|
|
15
|
+
import { type Grade, tierName } from "@/src/audit/scoring";
|
|
16
|
+
import { usePostHog } from "@/contexts/PostHogContext";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
result: AuditResult;
|
|
20
|
+
projected: number;
|
|
21
|
+
projectedGrade: Grade;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DETECTOR_TO_PRIMARY_POLICY: Record<string, string> = {
|
|
25
|
+
"redundant-cd-cwd": "warn-repeated-tool-calls",
|
|
26
|
+
"prefer-edit-over-read-cat": "block-read-outside-cwd",
|
|
27
|
+
"prefer-edit-over-sed-awk": "warn-repeated-tool-calls",
|
|
28
|
+
"prefer-write-over-heredoc": "block-env-files",
|
|
29
|
+
"sleep-polling-loop": "warn-background-process",
|
|
30
|
+
"find-from-root": "block-read-outside-cwd",
|
|
31
|
+
"git-commit-no-verify": "warn-git-amend",
|
|
32
|
+
"reread-after-edit": "warn-repeated-tool-calls",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const POLICY_DESC: Record<string, string> = {
|
|
36
|
+
"warn-repeated-tool-calls": "warns when the same tool is called 3+ times with identical parameters.",
|
|
37
|
+
"block-read-outside-cwd": "denies any file read outside the project root, including symlinks.",
|
|
38
|
+
"block-env-files": "blocks reads and writes of .env files at the tool layer.",
|
|
39
|
+
"block-secrets-write": "blocks writes to .pem, id_rsa, credentials.json, and other secret-key files.",
|
|
40
|
+
"warn-background-process": "warns before starting nohup / & / screen / tmux processes.",
|
|
41
|
+
"warn-git-amend": "warns before amending git commits.",
|
|
42
|
+
"require-ci-green-before-stop": "requires CI checks to pass on HEAD before the agent declares the task done.",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function shortName(name: string): string {
|
|
46
|
+
const slash = name.indexOf("/");
|
|
47
|
+
return slash >= 0 ? name.slice(slash + 1) : name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface FixRow {
|
|
51
|
+
name: string;
|
|
52
|
+
desc: string;
|
|
53
|
+
hits: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildFixes(result: AuditResult): FixRow[] {
|
|
57
|
+
const enabledSet = new Set(result.enabledBuiltinNames ?? []);
|
|
58
|
+
const buckets = new Map<string, number>();
|
|
59
|
+
|
|
60
|
+
for (const row of result.results) {
|
|
61
|
+
if (row.hits === 0) continue;
|
|
62
|
+
|
|
63
|
+
let target: string;
|
|
64
|
+
if (row.source === "audit-detector") {
|
|
65
|
+
const mapped = DETECTOR_TO_PRIMARY_POLICY[shortName(row.name)];
|
|
66
|
+
if (!mapped) continue;
|
|
67
|
+
target = mapped;
|
|
68
|
+
} else if (row.source === "builtin" && !row.enabledInConfig) {
|
|
69
|
+
target = shortName(row.name);
|
|
70
|
+
} else {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (enabledSet.has(target)) continue;
|
|
75
|
+
buckets.set(target, (buckets.get(target) ?? 0) + row.hits);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [...buckets.entries()]
|
|
79
|
+
.sort((a, b) => b[1] - a[1])
|
|
80
|
+
.map(([name, hits]) => ({
|
|
81
|
+
name,
|
|
82
|
+
desc: POLICY_DESC[name] ?? "enable this builtin policy to close the gap.",
|
|
83
|
+
hits,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function bulkInstall(fixes: FixRow[]): string {
|
|
88
|
+
if (fixes.length === 0) return "";
|
|
89
|
+
if (fixes.length === 1) return `failproofai policy add ${fixes[0]!.name}`;
|
|
90
|
+
return `failproofai policy add ${fixes.map((f) => f.name).join(" ")}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function HowToImproveSection({ result, projected, projectedGrade }: Props) {
|
|
94
|
+
const { capture } = usePostHog();
|
|
95
|
+
const fixes = useMemo(() => buildFixes(result), [result]);
|
|
96
|
+
const installAllCmd = useMemo(() => bulkInstall(fixes), [fixes]);
|
|
97
|
+
const [copiedAll, setCopiedAll] = useState(false);
|
|
98
|
+
|
|
99
|
+
if (fixes.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const handleInstallAll = async () => {
|
|
102
|
+
try {
|
|
103
|
+
await navigator.clipboard.writeText(installAllCmd);
|
|
104
|
+
setCopiedAll(true);
|
|
105
|
+
capture("audit_copy_clicked", {
|
|
106
|
+
source: "how_to_improve_section_install_all",
|
|
107
|
+
item_type: "bulk_install_command",
|
|
108
|
+
policy_count: fixes.length,
|
|
109
|
+
});
|
|
110
|
+
setTimeout(() => setCopiedAll(false), 1500);
|
|
111
|
+
} catch { /* ignore */ }
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<section className="audit-sec" data-screen-label="04 How to improve">
|
|
116
|
+
<div className="audit-sec-head">
|
|
117
|
+
<span className="audit-sec-eyebrow">
|
|
118
|
+
<span className="ix">04</span>{"// how to improve"}
|
|
119
|
+
</span>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className="install-all-btn"
|
|
123
|
+
onClick={handleInstallAll}
|
|
124
|
+
aria-label="Copy install-all command"
|
|
125
|
+
>
|
|
126
|
+
{copiedAll ? "copied" : "install all"}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
<h2 className="audit-sec-title">install or configure</h2>
|
|
130
|
+
<div className="audit-sec-sub">
|
|
131
|
+
enable all {fixes.length === 1 ? "one" : fixes.length} → projected{" "}
|
|
132
|
+
<strong>{projected}</strong> · {tierName(projectedGrade).toLowerCase()}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="fix-list">
|
|
136
|
+
{fixes.map((f, i) => (
|
|
137
|
+
<FixRow key={f.name} fix={f} idx={i} />
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function FixRow({ fix, idx }: { fix: FixRow; idx: number }) {
|
|
145
|
+
const { capture } = usePostHog();
|
|
146
|
+
const [copied, setCopied] = useState(false);
|
|
147
|
+
const install = `failproofai policy add ${fix.name}`;
|
|
148
|
+
|
|
149
|
+
const handleCopy = async () => {
|
|
150
|
+
try {
|
|
151
|
+
await navigator.clipboard.writeText(install);
|
|
152
|
+
setCopied(true);
|
|
153
|
+
capture("audit_copy_clicked", {
|
|
154
|
+
source: "how_to_improve_section",
|
|
155
|
+
item_type: "single_policy_install_command",
|
|
156
|
+
policy_name: fix.name,
|
|
157
|
+
policy_rank: idx + 1,
|
|
158
|
+
});
|
|
159
|
+
setTimeout(() => setCopied(false), 1500);
|
|
160
|
+
} catch { /* ignore */ }
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="fix-row">
|
|
165
|
+
<div className="fix-row-info">
|
|
166
|
+
<div className="fix-name">{fix.name}</div>
|
|
167
|
+
<div className="fix-desc">{fix.desc}</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="fix-row-cmd">
|
|
170
|
+
<code className="fix-cmd-code">{install}</code>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className="copy-icon-btn"
|
|
174
|
+
onClick={handleCopy}
|
|
175
|
+
aria-label={`Copy install command for ${fix.name}`}
|
|
176
|
+
>
|
|
177
|
+
{copied ? "✓" : (
|
|
178
|
+
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
|
|
179
|
+
<rect x="3" y="3" width="9" height="11" fill="none" stroke="currentColor" strokeWidth="1.2" />
|
|
180
|
+
<rect x="5.5" y="0.5" width="9" height="11" fill="none" stroke="currentColor" strokeWidth="1.2" />
|
|
181
|
+
</svg>
|
|
182
|
+
)}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|